diff options
Diffstat (limited to 'packages/tests')
377 files changed, 62284 insertions, 0 deletions
diff --git a/packages/tests/fixtures/60fps_720p_small.mp4 b/packages/tests/fixtures/60fps_720p_small.mp4 new file mode 100644 index 000000000..74bf968a4 --- /dev/null +++ b/packages/tests/fixtures/60fps_720p_small.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/ap-json/mastodon/bad-body-http-signature.json b/packages/tests/fixtures/ap-json/mastodon/bad-body-http-signature.json new file mode 100644 index 000000000..4e7bc3af5 --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/ap-json/mastodon/bad-http-signature.json b/packages/tests/fixtures/ap-json/mastodon/bad-http-signature.json new file mode 100644 index 000000000..098597db0 --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/ap-json/mastodon/bad-public-key.json b/packages/tests/fixtures/ap-json/mastodon/bad-public-key.json new file mode 100644 index 000000000..73d18b3ad --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/ap-json/mastodon/create-bad-signature.json b/packages/tests/fixtures/ap-json/mastodon/create-bad-signature.json new file mode 100644 index 000000000..2cd037241 --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/ap-json/mastodon/create.json b/packages/tests/fixtures/ap-json/mastodon/create.json new file mode 100644 index 000000000..0be271bb8 --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/ap-json/mastodon/http-signature.json b/packages/tests/fixtures/ap-json/mastodon/http-signature.json new file mode 100644 index 000000000..4e7bc3af5 --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/ap-json/mastodon/public-key.json b/packages/tests/fixtures/ap-json/mastodon/public-key.json new file mode 100644 index 000000000..b7b9b8308 --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/ap-json/peertube/announce-without-context.json b/packages/tests/fixtures/ap-json/peertube/announce-without-context.json new file mode 100644 index 000000000..cda1c514c --- /dev/null +++ b/packages/tests/fixtures/ap-json/peertube/announce-without-context.json | |||
@@ -0,0 +1,13 @@ | |||
1 | { | ||
2 | "type": "Announce", | ||
3 | "id": "http://127.0.0.1:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05/announces/1", | ||
4 | "actor": "http://127.0.0.1:9002/accounts/peertube", | ||
5 | "object": "http://127.0.0.1:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05", | ||
6 | "to": [ | ||
7 | "https://www.w3.org/ns/activitystreams#Public", | ||
8 | "http://127.0.0.1:9002/accounts/peertube/followers", | ||
9 | "http://127.0.0.1:9002/video-channels/root_channel/followers", | ||
10 | "http://127.0.0.1:9002/accounts/root/followers" | ||
11 | ], | ||
12 | "cc": [] | ||
13 | } | ||
diff --git a/packages/tests/fixtures/ap-json/peertube/invalid-keys.json b/packages/tests/fixtures/ap-json/peertube/invalid-keys.json new file mode 100644 index 000000000..0544e96b9 --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/ap-json/peertube/keys.json b/packages/tests/fixtures/ap-json/peertube/keys.json new file mode 100644 index 000000000..1a7700865 --- /dev/null +++ b/packages/tests/fixtures/ap-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/packages/tests/fixtures/avatar-big.png b/packages/tests/fixtures/avatar-big.png new file mode 100644 index 000000000..e593e40da --- /dev/null +++ b/packages/tests/fixtures/avatar-big.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar-resized-120x120.gif b/packages/tests/fixtures/avatar-resized-120x120.gif new file mode 100644 index 000000000..81a82189e --- /dev/null +++ b/packages/tests/fixtures/avatar-resized-120x120.gif | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar-resized-120x120.png b/packages/tests/fixtures/avatar-resized-120x120.png new file mode 100644 index 000000000..9d84151f8 --- /dev/null +++ b/packages/tests/fixtures/avatar-resized-120x120.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar-resized-48x48.gif b/packages/tests/fixtures/avatar-resized-48x48.gif new file mode 100644 index 000000000..5900ff12e --- /dev/null +++ b/packages/tests/fixtures/avatar-resized-48x48.gif | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar-resized-48x48.png b/packages/tests/fixtures/avatar-resized-48x48.png new file mode 100644 index 000000000..9e5f3b490 --- /dev/null +++ b/packages/tests/fixtures/avatar-resized-48x48.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar.gif b/packages/tests/fixtures/avatar.gif new file mode 100644 index 000000000..f29707760 --- /dev/null +++ b/packages/tests/fixtures/avatar.gif | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar.png b/packages/tests/fixtures/avatar.png new file mode 100644 index 000000000..4b7fd2c0a --- /dev/null +++ b/packages/tests/fixtures/avatar.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar2-resized-120x120.png b/packages/tests/fixtures/avatar2-resized-120x120.png new file mode 100644 index 000000000..44149facb --- /dev/null +++ b/packages/tests/fixtures/avatar2-resized-120x120.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar2-resized-48x48.png b/packages/tests/fixtures/avatar2-resized-48x48.png new file mode 100644 index 000000000..bb3939b1a --- /dev/null +++ b/packages/tests/fixtures/avatar2-resized-48x48.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/avatar2.png b/packages/tests/fixtures/avatar2.png new file mode 100644 index 000000000..dae702190 --- /dev/null +++ b/packages/tests/fixtures/avatar2.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/banner-resized.jpg b/packages/tests/fixtures/banner-resized.jpg new file mode 100644 index 000000000..952732d61 --- /dev/null +++ b/packages/tests/fixtures/banner-resized.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/banner.jpg b/packages/tests/fixtures/banner.jpg new file mode 100644 index 000000000..e5f284f59 --- /dev/null +++ b/packages/tests/fixtures/banner.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/custom-preview-big.png b/packages/tests/fixtures/custom-preview-big.png new file mode 100644 index 000000000..03d171af3 --- /dev/null +++ b/packages/tests/fixtures/custom-preview-big.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/custom-preview.jpg b/packages/tests/fixtures/custom-preview.jpg new file mode 100644 index 000000000..5a039d830 --- /dev/null +++ b/packages/tests/fixtures/custom-preview.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/custom-thumbnail-big.jpg b/packages/tests/fixtures/custom-thumbnail-big.jpg new file mode 100644 index 000000000..08375e425 --- /dev/null +++ b/packages/tests/fixtures/custom-thumbnail-big.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/custom-thumbnail.jpg b/packages/tests/fixtures/custom-thumbnail.jpg new file mode 100644 index 000000000..ef818442d --- /dev/null +++ b/packages/tests/fixtures/custom-thumbnail.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/custom-thumbnail.png b/packages/tests/fixtures/custom-thumbnail.png new file mode 100644 index 000000000..9f34daec1 --- /dev/null +++ b/packages/tests/fixtures/custom-thumbnail.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/exif.jpg b/packages/tests/fixtures/exif.jpg new file mode 100644 index 000000000..2997b38e9 --- /dev/null +++ b/packages/tests/fixtures/exif.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/exif.png b/packages/tests/fixtures/exif.png new file mode 100644 index 000000000..a1a0113f8 --- /dev/null +++ b/packages/tests/fixtures/exif.png | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/0-000067.ts b/packages/tests/fixtures/live/0-000067.ts new file mode 100644 index 000000000..a59f41a63 --- /dev/null +++ b/packages/tests/fixtures/live/0-000067.ts | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/0-000068.ts b/packages/tests/fixtures/live/0-000068.ts new file mode 100644 index 000000000..83dcbbb4c --- /dev/null +++ b/packages/tests/fixtures/live/0-000068.ts | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/0-000069.ts b/packages/tests/fixtures/live/0-000069.ts new file mode 100644 index 000000000..cafd4e978 --- /dev/null +++ b/packages/tests/fixtures/live/0-000069.ts | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/0-000070.ts b/packages/tests/fixtures/live/0-000070.ts new file mode 100644 index 000000000..0936199ea --- /dev/null +++ b/packages/tests/fixtures/live/0-000070.ts | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/0.m3u8 b/packages/tests/fixtures/live/0.m3u8 new file mode 100644 index 000000000..c3be19d26 --- /dev/null +++ b/packages/tests/fixtures/live/0.m3u8 | |||
@@ -0,0 +1,14 @@ | |||
1 | #EXTM3U | ||
2 | #EXT-X-VERSION:6 | ||
3 | #EXT-X-TARGETDURATION:2 | ||
4 | #EXT-X-MEDIA-SEQUENCE:68 | ||
5 | #EXT-X-INDEPENDENT-SEGMENTS | ||
6 | #EXTINF:2.000000, | ||
7 | #EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:39.019+0200 | ||
8 | 0-000068.ts | ||
9 | #EXTINF:2.000000, | ||
10 | #EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:41.019+0200 | ||
11 | 0-000069.ts | ||
12 | #EXTINF:2.000000, | ||
13 | #EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:43.019+0200 | ||
14 | 0-000070. | ||
diff --git a/packages/tests/fixtures/live/1-000067.ts b/packages/tests/fixtures/live/1-000067.ts new file mode 100644 index 000000000..17db8f81e --- /dev/null +++ b/packages/tests/fixtures/live/1-000067.ts | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/1-000068.ts b/packages/tests/fixtures/live/1-000068.ts new file mode 100644 index 000000000..f7bb97040 --- /dev/null +++ b/packages/tests/fixtures/live/1-000068.ts | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/1-000069.ts b/packages/tests/fixtures/live/1-000069.ts new file mode 100644 index 000000000..64c791337 --- /dev/null +++ b/packages/tests/fixtures/live/1-000069.ts | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/1-000070.ts b/packages/tests/fixtures/live/1-000070.ts new file mode 100644 index 000000000..a5f04f109 --- /dev/null +++ b/packages/tests/fixtures/live/1-000070.ts | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/live/1.m3u8 b/packages/tests/fixtures/live/1.m3u8 new file mode 100644 index 000000000..26d7fa6b0 --- /dev/null +++ b/packages/tests/fixtures/live/1.m3u8 | |||
@@ -0,0 +1,14 @@ | |||
1 | #EXTM3U | ||
2 | #EXT-X-VERSION:6 | ||
3 | #EXT-X-TARGETDURATION:2 | ||
4 | #EXT-X-MEDIA-SEQUENCE:68 | ||
5 | #EXT-X-INDEPENDENT-SEGMENTS | ||
6 | #EXTINF:2.000000, | ||
7 | #EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:39.019+0200 | ||
8 | 1-000068.ts | ||
9 | #EXTINF:2.000000, | ||
10 | #EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:41.019+0200 | ||
11 | 1-000069.ts | ||
12 | #EXTINF:2.000000, | ||
13 | #EXT-X-PROGRAM-DATE-TIME:2023-04-18T13:38:43.019+0200 | ||
14 | 1-000070.ts | ||
diff --git a/packages/tests/fixtures/live/master.m3u8 b/packages/tests/fixtures/live/master.m3u8 new file mode 100644 index 000000000..7e52f33cf --- /dev/null +++ b/packages/tests/fixtures/live/master.m3u8 | |||
@@ -0,0 +1,8 @@ | |||
1 | #EXTM3U | ||
2 | #EXT-X-VERSION:6 | ||
3 | #EXT-X-STREAM-INF:BANDWIDTH=1287342,RESOLUTION=640x360,CODECS="avc1.64001f,mp4a.40.2" | ||
4 | 0.m3u8 | ||
5 | |||
6 | #EXT-X-STREAM-INF:BANDWIDTH=3051742,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2" | ||
7 | 1.m3u8 | ||
8 | |||
diff --git a/packages/tests/fixtures/low-bitrate.mp4 b/packages/tests/fixtures/low-bitrate.mp4 new file mode 100644 index 000000000..69004eccc --- /dev/null +++ b/packages/tests/fixtures/low-bitrate.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/peertube-plugin-test-broken/main.js b/packages/tests/fixtures/peertube-plugin-test-broken/main.js new file mode 100644 index 000000000..afdb6f7a0 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-broken/main.js | |||
@@ -0,0 +1,12 @@ | |||
1 | async function register (options) { | ||
2 | options.unknownFunction() | ||
3 | } | ||
4 | |||
5 | async function unregister () { | ||
6 | return | ||
7 | } | ||
8 | |||
9 | module.exports = { | ||
10 | register, | ||
11 | unregister | ||
12 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-broken/package.json b/packages/tests/fixtures/peertube-plugin-test-broken/package.json new file mode 100644 index 000000000..fd03df216 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-broken/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-broken", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test broken", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-external-auth-one/main.js b/packages/tests/fixtures/peertube-plugin-test-external-auth-one/main.js new file mode 100644 index 000000000..58bc27661 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-external-auth-one/main.js | |||
@@ -0,0 +1,85 @@ | |||
1 | async function register ({ | ||
2 | registerExternalAuth, | ||
3 | peertubeHelpers, | ||
4 | settingsManager, | ||
5 | unregisterExternalAuth | ||
6 | }) { | ||
7 | { | ||
8 | const result = registerExternalAuth({ | ||
9 | authName: 'external-auth-1', | ||
10 | authDisplayName: () => 'External Auth 1', | ||
11 | onLogout: user => peertubeHelpers.logger.info('On logout %s', user.username), | ||
12 | onAuthRequest: (req, res) => { | ||
13 | const username = req.query.username | ||
14 | |||
15 | result.userAuthenticated({ | ||
16 | req, | ||
17 | res, | ||
18 | username, | ||
19 | email: username + '@example.com' | ||
20 | }) | ||
21 | } | ||
22 | }) | ||
23 | } | ||
24 | |||
25 | { | ||
26 | const result = registerExternalAuth({ | ||
27 | authName: 'external-auth-2', | ||
28 | authDisplayName: () => 'External Auth 2', | ||
29 | onAuthRequest: (req, res) => { | ||
30 | result.userAuthenticated({ | ||
31 | req, | ||
32 | res, | ||
33 | username: 'kefka', | ||
34 | email: 'kefka@example.com', | ||
35 | role: 0, | ||
36 | displayName: 'Kefka Palazzo', | ||
37 | adminFlags: 1, | ||
38 | videoQuota: 42000, | ||
39 | videoQuotaDaily: 42100, | ||
40 | |||
41 | // Always use new value except for videoQuotaDaily field | ||
42 | userUpdater: ({ fieldName, currentValue, newValue }) => { | ||
43 | if (fieldName === 'videoQuotaDaily') return currentValue | ||
44 | |||
45 | return newValue | ||
46 | } | ||
47 | }) | ||
48 | }, | ||
49 | hookTokenValidity: (options) => { | ||
50 | if (options.type === 'refresh') { | ||
51 | return { valid: false } | ||
52 | } | ||
53 | |||
54 | if (options.type === 'access') { | ||
55 | const token = options.token | ||
56 | const now = new Date() | ||
57 | now.setTime(now.getTime() - 5000) | ||
58 | |||
59 | const createdAt = new Date(token.createdAt) | ||
60 | |||
61 | return { valid: createdAt.getTime() >= now.getTime() } | ||
62 | } | ||
63 | |||
64 | return { valid: true } | ||
65 | } | ||
66 | }) | ||
67 | } | ||
68 | |||
69 | settingsManager.onSettingsChange(settings => { | ||
70 | if (settings.disableKefka) { | ||
71 | unregisterExternalAuth('external-auth-2') | ||
72 | } | ||
73 | }) | ||
74 | } | ||
75 | |||
76 | async function unregister () { | ||
77 | return | ||
78 | } | ||
79 | |||
80 | module.exports = { | ||
81 | register, | ||
82 | unregister | ||
83 | } | ||
84 | |||
85 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-external-auth-one/package.json b/packages/tests/fixtures/peertube-plugin-test-external-auth-one/package.json new file mode 100644 index 000000000..22814b047 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-external-auth-one/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-external-auth-one", | ||
3 | "version": "0.0.1", | ||
4 | "description": "External auth one", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-external-auth-three/main.js b/packages/tests/fixtures/peertube-plugin-test-external-auth-three/main.js new file mode 100644 index 000000000..30cedccc6 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-external-auth-three/main.js | |||
@@ -0,0 +1,53 @@ | |||
1 | async function register ({ | ||
2 | registerExternalAuth, | ||
3 | peertubeHelpers | ||
4 | }) { | ||
5 | { | ||
6 | const result = registerExternalAuth({ | ||
7 | authName: 'external-auth-7', | ||
8 | authDisplayName: () => 'External Auth 7', | ||
9 | onAuthRequest: (req, res) => { | ||
10 | result.userAuthenticated({ | ||
11 | req, | ||
12 | res, | ||
13 | username: 'cid', | ||
14 | email: 'cid@example.com', | ||
15 | displayName: 'Cid Marquez' | ||
16 | }) | ||
17 | }, | ||
18 | onLogout: (user, req) => { | ||
19 | return 'https://example.com/redirectUrl' | ||
20 | } | ||
21 | }) | ||
22 | } | ||
23 | |||
24 | { | ||
25 | const result = registerExternalAuth({ | ||
26 | authName: 'external-auth-8', | ||
27 | authDisplayName: () => 'External Auth 8', | ||
28 | onAuthRequest: (req, res) => { | ||
29 | result.userAuthenticated({ | ||
30 | req, | ||
31 | res, | ||
32 | username: 'cid', | ||
33 | email: 'cid@example.com', | ||
34 | displayName: 'Cid Marquez' | ||
35 | }) | ||
36 | }, | ||
37 | onLogout: (user, req) => { | ||
38 | return 'https://example.com/redirectUrl?access_token=' + req.headers['authorization'].split(' ')[1] | ||
39 | } | ||
40 | }) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | async function unregister () { | ||
45 | |||
46 | } | ||
47 | |||
48 | module.exports = { | ||
49 | register, | ||
50 | unregister | ||
51 | } | ||
52 | |||
53 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-external-auth-three/package.json b/packages/tests/fixtures/peertube-plugin-test-external-auth-three/package.json new file mode 100644 index 000000000..f323d189d --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-external-auth-three/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-external-auth-three", | ||
3 | "version": "0.0.1", | ||
4 | "description": "External auth three", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-external-auth-two/main.js b/packages/tests/fixtures/peertube-plugin-test-external-auth-two/main.js new file mode 100644 index 000000000..755dbb62b --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-external-auth-two/main.js | |||
@@ -0,0 +1,95 @@ | |||
1 | async function register ({ | ||
2 | registerExternalAuth, | ||
3 | peertubeHelpers | ||
4 | }) { | ||
5 | { | ||
6 | const result = registerExternalAuth({ | ||
7 | authName: 'external-auth-3', | ||
8 | authDisplayName: () => 'External Auth 3', | ||
9 | onAuthRequest: (req, res) => { | ||
10 | result.userAuthenticated({ | ||
11 | req, | ||
12 | res, | ||
13 | username: 'cid', | ||
14 | email: 'cid@example.com', | ||
15 | displayName: 'Cid Marquez' | ||
16 | }) | ||
17 | } | ||
18 | }) | ||
19 | } | ||
20 | |||
21 | { | ||
22 | const result = registerExternalAuth({ | ||
23 | authName: 'external-auth-4', | ||
24 | authDisplayName: () => 'External Auth 4', | ||
25 | onAuthRequest: (req, res) => { | ||
26 | result.userAuthenticated({ | ||
27 | req, | ||
28 | res, | ||
29 | username: 'kefka2', | ||
30 | email: 'kefka@example.com', | ||
31 | displayName: 'Kefka duplication' | ||
32 | }) | ||
33 | } | ||
34 | }) | ||
35 | } | ||
36 | |||
37 | { | ||
38 | const result = registerExternalAuth({ | ||
39 | authName: 'external-auth-5', | ||
40 | authDisplayName: () => 'External Auth 5', | ||
41 | onAuthRequest: (req, res) => { | ||
42 | result.userAuthenticated({ | ||
43 | req, | ||
44 | res, | ||
45 | username: 'kefka', | ||
46 | email: 'kefka@example.com', | ||
47 | displayName: 'Kefka duplication' | ||
48 | }) | ||
49 | } | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | { | ||
54 | const result = registerExternalAuth({ | ||
55 | authName: 'external-auth-6', | ||
56 | authDisplayName: () => 'External Auth 6', | ||
57 | onAuthRequest: (req, res) => { | ||
58 | result.userAuthenticated({ | ||
59 | req, | ||
60 | res, | ||
61 | username: 'existing_user', | ||
62 | email: 'existing_user@example.com', | ||
63 | displayName: 'Existing user' | ||
64 | }) | ||
65 | } | ||
66 | }) | ||
67 | } | ||
68 | |||
69 | { | ||
70 | const result = registerExternalAuth({ | ||
71 | authName: 'external-auth-7', | ||
72 | authDisplayName: () => 'External Auth 7', | ||
73 | onAuthRequest: (req, res) => { | ||
74 | result.userAuthenticated({ | ||
75 | req, | ||
76 | res, | ||
77 | username: 'existing_user2', | ||
78 | email: 'custom_email_existing_user2@example.com', | ||
79 | displayName: 'Existing user 2' | ||
80 | }) | ||
81 | } | ||
82 | }) | ||
83 | } | ||
84 | } | ||
85 | |||
86 | async function unregister () { | ||
87 | return | ||
88 | } | ||
89 | |||
90 | module.exports = { | ||
91 | register, | ||
92 | unregister | ||
93 | } | ||
94 | |||
95 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-external-auth-two/package.json b/packages/tests/fixtures/peertube-plugin-test-external-auth-two/package.json new file mode 100644 index 000000000..a5ca4d07a --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-external-auth-two/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-external-auth-two", | ||
3 | "version": "0.0.1", | ||
4 | "description": "External auth two", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-filter-translations/languages/fr.json b/packages/tests/fixtures/peertube-plugin-test-filter-translations/languages/fr.json new file mode 100644 index 000000000..52d8313df --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-filter-translations/languages/fr.json | |||
@@ -0,0 +1,3 @@ | |||
1 | { | ||
2 | "Hello world": "Bonjour le monde" | ||
3 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-filter-translations/languages/it.json b/packages/tests/fixtures/peertube-plugin-test-filter-translations/languages/it.json new file mode 100644 index 000000000..9e187d83b --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-filter-translations/languages/it.json | |||
@@ -0,0 +1,3 @@ | |||
1 | { | ||
2 | "Hello world": "Ciao, mondo!" | ||
3 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-filter-translations/main.js b/packages/tests/fixtures/peertube-plugin-test-filter-translations/main.js new file mode 100644 index 000000000..71c11b2ba --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-filter-translations/main.js | |||
@@ -0,0 +1,21 @@ | |||
1 | async function register ({ registerHook, registerSetting, settingsManager, storageManager, peertubeHelpers }) { | ||
2 | registerHook({ | ||
3 | target: 'filter:api.videos.list.params', | ||
4 | handler: obj => addToCount(obj) | ||
5 | }) | ||
6 | } | ||
7 | |||
8 | async function unregister () { | ||
9 | return | ||
10 | } | ||
11 | |||
12 | module.exports = { | ||
13 | register, | ||
14 | unregister | ||
15 | } | ||
16 | |||
17 | // ############################################################################ | ||
18 | |||
19 | function addToCount (obj) { | ||
20 | return Object.assign({}, obj, { count: obj.count + 1 }) | ||
21 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-filter-translations/package.json b/packages/tests/fixtures/peertube-plugin-test-filter-translations/package.json new file mode 100644 index 000000000..2adce4743 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-filter-translations/package.json | |||
@@ -0,0 +1,23 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-filter-translations", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test filter and translations", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": { | ||
20 | "fr-FR": "./languages/fr.json", | ||
21 | "it-IT": "./languages/it.json" | ||
22 | } | ||
23 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-five/main.js b/packages/tests/fixtures/peertube-plugin-test-five/main.js new file mode 100644 index 000000000..07dd18654 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-five/main.js | |||
@@ -0,0 +1,23 @@ | |||
1 | async function register ({ | ||
2 | getRouter | ||
3 | }) { | ||
4 | const router = getRouter() | ||
5 | router.get('/ping', (req, res) => res.json({ message: 'pong' })) | ||
6 | |||
7 | router.get('/is-authenticated', (req, res) => res.json({ isAuthenticated: res.locals.authenticated })) | ||
8 | |||
9 | router.post('/form/post/mirror', (req, res) => { | ||
10 | res.json(req.body) | ||
11 | }) | ||
12 | } | ||
13 | |||
14 | async function unregister () { | ||
15 | return | ||
16 | } | ||
17 | |||
18 | module.exports = { | ||
19 | register, | ||
20 | unregister | ||
21 | } | ||
22 | |||
23 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-five/package.json b/packages/tests/fixtures/peertube-plugin-test-five/package.json new file mode 100644 index 000000000..1f5d65d9d --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-five/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-five", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test 5", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-four/main.js b/packages/tests/fixtures/peertube-plugin-test-four/main.js new file mode 100644 index 000000000..b10177b45 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-four/main.js | |||
@@ -0,0 +1,201 @@ | |||
1 | async function register ({ | ||
2 | peertubeHelpers, | ||
3 | registerHook, | ||
4 | getRouter | ||
5 | }) { | ||
6 | const logger = peertubeHelpers.logger | ||
7 | |||
8 | logger.info('Hello world from plugin four') | ||
9 | |||
10 | { | ||
11 | const username = 'root' | ||
12 | const results = await peertubeHelpers.database.query( | ||
13 | 'SELECT "email" from "user" WHERE "username" = $username', | ||
14 | { | ||
15 | type: 'SELECT', | ||
16 | bind: { username } | ||
17 | } | ||
18 | ) | ||
19 | |||
20 | logger.info('root email is ' + results[0]['email']) | ||
21 | } | ||
22 | |||
23 | { | ||
24 | registerHook({ | ||
25 | target: 'action:api.video.viewed', | ||
26 | handler: async ({ video }) => { | ||
27 | const videoFromDB1 = await peertubeHelpers.videos.loadByUrl(video.url) | ||
28 | const videoFromDB2 = await peertubeHelpers.videos.loadByIdOrUUID(video.id) | ||
29 | const videoFromDB3 = await peertubeHelpers.videos.loadByIdOrUUID(video.uuid) | ||
30 | |||
31 | if (videoFromDB1.uuid !== videoFromDB2.uuid || videoFromDB2.uuid !== videoFromDB3.uuid) return | ||
32 | |||
33 | logger.info('video from DB uuid is %s.', videoFromDB1.uuid) | ||
34 | |||
35 | await peertubeHelpers.videos.removeVideo(video.id) | ||
36 | |||
37 | logger.info('Video deleted by plugin four.') | ||
38 | } | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | { | ||
43 | const serverActor = await peertubeHelpers.server.getServerActor() | ||
44 | logger.info('server actor name is %s', serverActor.preferredUsername) | ||
45 | } | ||
46 | |||
47 | { | ||
48 | logger.info('server url is %s', peertubeHelpers.config.getWebserverUrl()) | ||
49 | } | ||
50 | |||
51 | { | ||
52 | const actions = { | ||
53 | blockServer, | ||
54 | unblockServer, | ||
55 | blockAccount, | ||
56 | unblockAccount, | ||
57 | blacklist, | ||
58 | unblacklist | ||
59 | } | ||
60 | |||
61 | const router = getRouter() | ||
62 | router.post('/commander', async (req, res) => { | ||
63 | try { | ||
64 | await actions[req.body.command](peertubeHelpers, req.body) | ||
65 | |||
66 | res.sendStatus(204) | ||
67 | } catch (err) { | ||
68 | logger.error('Error in commander.', { err }) | ||
69 | res.sendStatus(500) | ||
70 | } | ||
71 | }) | ||
72 | |||
73 | router.get('/server-config', async (req, res) => { | ||
74 | const serverConfig = await peertubeHelpers.config.getServerConfig() | ||
75 | |||
76 | return res.json({ serverConfig }) | ||
77 | }) | ||
78 | |||
79 | router.get('/server-listening-config', async (req, res) => { | ||
80 | const config = await peertubeHelpers.config.getServerListeningConfig() | ||
81 | |||
82 | return res.json({ config }) | ||
83 | }) | ||
84 | |||
85 | router.get('/static-route', async (req, res) => { | ||
86 | const staticRoute = peertubeHelpers.plugin.getBaseStaticRoute() | ||
87 | |||
88 | return res.json({ staticRoute }) | ||
89 | }) | ||
90 | |||
91 | router.get('/router-route', async (req, res) => { | ||
92 | const routerRoute = peertubeHelpers.plugin.getBaseRouterRoute() | ||
93 | |||
94 | return res.json({ routerRoute }) | ||
95 | }) | ||
96 | |||
97 | router.get('/user/:id', async (req, res) => { | ||
98 | const user = await peertubeHelpers.user.loadById(req.params.id) | ||
99 | if (!user) return res.status(404).end() | ||
100 | |||
101 | return res.json({ | ||
102 | username: user.username | ||
103 | }) | ||
104 | }) | ||
105 | |||
106 | router.get('/user', async (req, res) => { | ||
107 | const user = await peertubeHelpers.user.getAuthUser(res) | ||
108 | if (!user) return res.sendStatus(404) | ||
109 | |||
110 | const isAdmin = user.role === 0 | ||
111 | const isModerator = user.role === 1 | ||
112 | const isUser = user.role === 2 | ||
113 | |||
114 | return res.json({ | ||
115 | id: user.id, | ||
116 | username: user.username, | ||
117 | displayName: user.Account.name, | ||
118 | isAdmin, | ||
119 | isModerator, | ||
120 | isUser | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | router.get('/video-files/:id', async (req, res) => { | ||
125 | const details = await peertubeHelpers.videos.getFiles(req.params.id) | ||
126 | if (!details) return res.sendStatus(404) | ||
127 | |||
128 | return res.json(details) | ||
129 | }) | ||
130 | |||
131 | router.get('/ffprobe', async (req, res) => { | ||
132 | const result = await peertubeHelpers.videos.ffprobe(req.query.path) | ||
133 | if (!result) return res.sendStatus(404) | ||
134 | |||
135 | return res.json(result) | ||
136 | }) | ||
137 | |||
138 | router.post('/send-notification', async (req, res) => { | ||
139 | peertubeHelpers.socket.sendNotification(req.body.userId, { | ||
140 | type: 1, | ||
141 | userId: req.body.userId | ||
142 | }) | ||
143 | |||
144 | return res.sendStatus(201) | ||
145 | }) | ||
146 | |||
147 | router.post('/send-video-live-new-state/:uuid', async (req, res) => { | ||
148 | const video = await peertubeHelpers.videos.loadByIdOrUUID(req.params.uuid) | ||
149 | peertubeHelpers.socket.sendVideoLiveNewState(video) | ||
150 | |||
151 | return res.sendStatus(201) | ||
152 | }) | ||
153 | } | ||
154 | |||
155 | } | ||
156 | |||
157 | async function unregister () { | ||
158 | return | ||
159 | } | ||
160 | |||
161 | module.exports = { | ||
162 | register, | ||
163 | unregister | ||
164 | } | ||
165 | |||
166 | // ########################################################################### | ||
167 | |||
168 | async function blockServer (peertubeHelpers, body) { | ||
169 | const serverActor = await peertubeHelpers.server.getServerActor() | ||
170 | |||
171 | await peertubeHelpers.moderation.blockServer({ byAccountId: serverActor.Account.id, hostToBlock: body.hostToBlock }) | ||
172 | } | ||
173 | |||
174 | async function unblockServer (peertubeHelpers, body) { | ||
175 | const serverActor = await peertubeHelpers.server.getServerActor() | ||
176 | |||
177 | await peertubeHelpers.moderation.unblockServer({ byAccountId: serverActor.Account.id, hostToUnblock: body.hostToUnblock }) | ||
178 | } | ||
179 | |||
180 | async function blockAccount (peertubeHelpers, body) { | ||
181 | const serverActor = await peertubeHelpers.server.getServerActor() | ||
182 | |||
183 | await peertubeHelpers.moderation.blockAccount({ byAccountId: serverActor.Account.id, handleToBlock: body.handleToBlock }) | ||
184 | } | ||
185 | |||
186 | async function unblockAccount (peertubeHelpers, body) { | ||
187 | const serverActor = await peertubeHelpers.server.getServerActor() | ||
188 | |||
189 | await peertubeHelpers.moderation.unblockAccount({ byAccountId: serverActor.Account.id, handleToUnblock: body.handleToUnblock }) | ||
190 | } | ||
191 | |||
192 | async function blacklist (peertubeHelpers, body) { | ||
193 | await peertubeHelpers.moderation.blacklistVideo({ | ||
194 | videoIdOrUUID: body.videoUUID, | ||
195 | createOptions: body | ||
196 | }) | ||
197 | } | ||
198 | |||
199 | async function unblacklist (peertubeHelpers, body) { | ||
200 | await peertubeHelpers.moderation.unblacklistVideo({ videoIdOrUUID: body.videoUUID }) | ||
201 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-four/package.json b/packages/tests/fixtures/peertube-plugin-test-four/package.json new file mode 100644 index 000000000..dda3c7f37 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-four/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-four", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test 4", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js new file mode 100644 index 000000000..f58faa847 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js | |||
@@ -0,0 +1,69 @@ | |||
1 | async function register ({ | ||
2 | registerIdAndPassAuth, | ||
3 | peertubeHelpers, | ||
4 | settingsManager, | ||
5 | unregisterIdAndPassAuth | ||
6 | }) { | ||
7 | registerIdAndPassAuth({ | ||
8 | authName: 'spyro-auth', | ||
9 | |||
10 | onLogout: () => { | ||
11 | peertubeHelpers.logger.info('On logout for auth 1 - 1') | ||
12 | }, | ||
13 | |||
14 | getWeight: () => 15, | ||
15 | |||
16 | login (body) { | ||
17 | if (body.id === 'spyro' && body.password === 'spyro password') { | ||
18 | return Promise.resolve({ | ||
19 | username: 'spyro', | ||
20 | email: 'spyro@example.com', | ||
21 | role: 2, | ||
22 | displayName: 'Spyro the Dragon' | ||
23 | }) | ||
24 | } | ||
25 | |||
26 | return null | ||
27 | } | ||
28 | }) | ||
29 | |||
30 | registerIdAndPassAuth({ | ||
31 | authName: 'crash-auth', | ||
32 | |||
33 | onLogout: () => { | ||
34 | peertubeHelpers.logger.info('On logout for auth 1 - 2') | ||
35 | }, | ||
36 | |||
37 | getWeight: () => 50, | ||
38 | |||
39 | login (body) { | ||
40 | if (body.id === 'crash' && body.password === 'crash password') { | ||
41 | return Promise.resolve({ | ||
42 | username: 'crash', | ||
43 | email: 'crash@example.com', | ||
44 | role: 1, | ||
45 | displayName: 'Crash Bandicoot' | ||
46 | }) | ||
47 | } | ||
48 | |||
49 | return null | ||
50 | } | ||
51 | }) | ||
52 | |||
53 | settingsManager.onSettingsChange(settings => { | ||
54 | if (settings.disableSpyro) { | ||
55 | unregisterIdAndPassAuth('spyro-auth') | ||
56 | } | ||
57 | }) | ||
58 | } | ||
59 | |||
60 | async function unregister () { | ||
61 | return | ||
62 | } | ||
63 | |||
64 | module.exports = { | ||
65 | register, | ||
66 | unregister | ||
67 | } | ||
68 | |||
69 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json new file mode 100644 index 000000000..f8ad18a90 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-id-pass-auth-one", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Id and pass auth one", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js new file mode 100644 index 000000000..1200acfbd --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js | |||
@@ -0,0 +1,106 @@ | |||
1 | async function register ({ | ||
2 | registerIdAndPassAuth, | ||
3 | peertubeHelpers | ||
4 | }) { | ||
5 | registerIdAndPassAuth({ | ||
6 | authName: 'laguna-bad-auth', | ||
7 | |||
8 | onLogout: () => { | ||
9 | peertubeHelpers.logger.info('On logout for auth 3 - 1') | ||
10 | }, | ||
11 | |||
12 | getWeight: () => 5, | ||
13 | |||
14 | login (body) { | ||
15 | if (body.id === 'laguna' && body.password === 'laguna password') { | ||
16 | return Promise.resolve({ | ||
17 | username: 'laguna', | ||
18 | email: 'laguna@example.com', | ||
19 | displayName: 'Laguna Loire' | ||
20 | }) | ||
21 | } | ||
22 | |||
23 | return null | ||
24 | } | ||
25 | }) | ||
26 | |||
27 | registerIdAndPassAuth({ | ||
28 | authName: 'ward-auth', | ||
29 | |||
30 | getWeight: () => 5, | ||
31 | |||
32 | login (body) { | ||
33 | if (body.id === 'ward') { | ||
34 | return Promise.resolve({ | ||
35 | username: '-ward-42', | ||
36 | email: 'ward@example.com' | ||
37 | }) | ||
38 | } | ||
39 | |||
40 | return null | ||
41 | } | ||
42 | }) | ||
43 | |||
44 | registerIdAndPassAuth({ | ||
45 | authName: 'kiros-auth', | ||
46 | |||
47 | getWeight: () => 5, | ||
48 | |||
49 | login (body) { | ||
50 | if (body.id === 'kiros') { | ||
51 | return Promise.resolve({ | ||
52 | username: 'kiros', | ||
53 | email: 'kiros@example.com', | ||
54 | displayName: 'a'.repeat(5000) | ||
55 | }) | ||
56 | } | ||
57 | |||
58 | return null | ||
59 | } | ||
60 | }) | ||
61 | |||
62 | registerIdAndPassAuth({ | ||
63 | authName: 'raine-auth', | ||
64 | |||
65 | getWeight: () => 5, | ||
66 | |||
67 | login (body) { | ||
68 | if (body.id === 'raine') { | ||
69 | return Promise.resolve({ | ||
70 | username: 'raine', | ||
71 | email: 'raine@example.com', | ||
72 | role: 42 | ||
73 | }) | ||
74 | } | ||
75 | |||
76 | return null | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | registerIdAndPassAuth({ | ||
81 | authName: 'ellone-auth', | ||
82 | |||
83 | getWeight: () => 5, | ||
84 | |||
85 | login (body) { | ||
86 | if (body.id === 'ellone') { | ||
87 | return Promise.resolve({ | ||
88 | username: 'ellone' | ||
89 | }) | ||
90 | } | ||
91 | |||
92 | return null | ||
93 | } | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | async function unregister () { | ||
98 | return | ||
99 | } | ||
100 | |||
101 | module.exports = { | ||
102 | register, | ||
103 | unregister | ||
104 | } | ||
105 | |||
106 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json new file mode 100644 index 000000000..f9f107b1a --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-id-pass-auth-three", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Id and pass auth three", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js new file mode 100644 index 000000000..fad5abf60 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js | |||
@@ -0,0 +1,65 @@ | |||
1 | async function register ({ | ||
2 | registerIdAndPassAuth, | ||
3 | peertubeHelpers | ||
4 | }) { | ||
5 | registerIdAndPassAuth({ | ||
6 | authName: 'laguna-auth', | ||
7 | |||
8 | onLogout: () => { | ||
9 | peertubeHelpers.logger.info('On logout for auth 2 - 1') | ||
10 | }, | ||
11 | |||
12 | getWeight: () => 30, | ||
13 | |||
14 | hookTokenValidity: (options) => { | ||
15 | if (options.type === 'refresh') { | ||
16 | return { valid: false } | ||
17 | } | ||
18 | |||
19 | if (options.type === 'access') { | ||
20 | const token = options.token | ||
21 | const now = new Date() | ||
22 | now.setTime(now.getTime() - 5000) | ||
23 | |||
24 | const createdAt = new Date(token.createdAt) | ||
25 | |||
26 | return { valid: createdAt.getTime() >= now.getTime() } | ||
27 | } | ||
28 | |||
29 | return { valid: true } | ||
30 | }, | ||
31 | |||
32 | login (body) { | ||
33 | if (body.id === 'laguna' && body.password === 'laguna password') { | ||
34 | return Promise.resolve({ | ||
35 | username: 'laguna', | ||
36 | email: 'laguna@example.com', | ||
37 | displayName: 'Laguna Loire', | ||
38 | adminFlags: 1, | ||
39 | videoQuota: 42000, | ||
40 | videoQuotaDaily: 42100, | ||
41 | |||
42 | // Always use new value except for videoQuotaDaily field | ||
43 | userUpdater: ({ fieldName, currentValue, newValue }) => { | ||
44 | if (fieldName === 'videoQuotaDaily') return currentValue | ||
45 | |||
46 | return newValue | ||
47 | } | ||
48 | }) | ||
49 | } | ||
50 | |||
51 | return null | ||
52 | } | ||
53 | }) | ||
54 | } | ||
55 | |||
56 | async function unregister () { | ||
57 | return | ||
58 | } | ||
59 | |||
60 | module.exports = { | ||
61 | register, | ||
62 | unregister | ||
63 | } | ||
64 | |||
65 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json new file mode 100644 index 000000000..5df15fac1 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-id-pass-auth-two", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Id and pass auth two", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-native/main.js b/packages/tests/fixtures/peertube-plugin-test-native/main.js new file mode 100644 index 000000000..0390faea9 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-native/main.js | |||
@@ -0,0 +1,21 @@ | |||
1 | const print = require('a-native-example') | ||
2 | |||
3 | async function register ({ getRouter }) { | ||
4 | print('hello world') | ||
5 | |||
6 | const router = getRouter() | ||
7 | |||
8 | router.get('/', (req, res) => { | ||
9 | print('hello world') | ||
10 | res.sendStatus(204) | ||
11 | }) | ||
12 | } | ||
13 | |||
14 | async function unregister () { | ||
15 | return | ||
16 | } | ||
17 | |||
18 | module.exports = { | ||
19 | register, | ||
20 | unregister | ||
21 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-native/package.json b/packages/tests/fixtures/peertube-plugin-test-native/package.json new file mode 100644 index 000000000..a6525720b --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-native/package.json | |||
@@ -0,0 +1,23 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-native", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test-native", | ||
5 | "engine": { | ||
6 | "peertube": ">=4.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {}, | ||
20 | "dependencies": { | ||
21 | "a-native-example": "^1.0.0" | ||
22 | } | ||
23 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-podcast-custom-tags/main.js b/packages/tests/fixtures/peertube-plugin-test-podcast-custom-tags/main.js new file mode 100644 index 000000000..ada4a70fe --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-podcast-custom-tags/main.js | |||
@@ -0,0 +1,82 @@ | |||
1 | async function register ({ registerHook, registerSetting, settingsManager, storageManager, peertubeHelpers }) { | ||
2 | registerHook({ | ||
3 | target: 'filter:feed.podcast.rss.create-custom-xmlns.result', | ||
4 | handler: (result, params) => { | ||
5 | return result.concat([ | ||
6 | { | ||
7 | name: "biz", | ||
8 | value: "https://example.com/biz-xmlns", | ||
9 | }, | ||
10 | ]) | ||
11 | } | ||
12 | }) | ||
13 | |||
14 | registerHook({ | ||
15 | target: 'filter:feed.podcast.channel.create-custom-tags.result', | ||
16 | handler: (result, params) => { | ||
17 | const { videoChannel } = params | ||
18 | return result.concat([ | ||
19 | { | ||
20 | name: "fooTag", | ||
21 | attributes: { "bar": "baz" }, | ||
22 | value: "42", | ||
23 | }, | ||
24 | { | ||
25 | name: "biz:videoChannel", | ||
26 | attributes: { "name": videoChannel.name, "id": videoChannel.id }, | ||
27 | }, | ||
28 | { | ||
29 | name: "biz:buzzItem", | ||
30 | value: [ | ||
31 | { | ||
32 | name: "nestedTag", | ||
33 | value: "example nested tag", | ||
34 | }, | ||
35 | ], | ||
36 | }, | ||
37 | ]) | ||
38 | } | ||
39 | }) | ||
40 | |||
41 | registerHook({ | ||
42 | target: 'filter:feed.podcast.video.create-custom-tags.result', | ||
43 | handler: (result, params) => { | ||
44 | const { video, liveItem } = params | ||
45 | return result.concat([ | ||
46 | { | ||
47 | name: "fizzTag", | ||
48 | attributes: { "bar": "baz" }, | ||
49 | value: "21", | ||
50 | }, | ||
51 | { | ||
52 | name: "biz:video", | ||
53 | attributes: { "name": video.name, "id": video.id, "isLive": liveItem }, | ||
54 | }, | ||
55 | { | ||
56 | name: "biz:buzz", | ||
57 | value: [ | ||
58 | { | ||
59 | name: "nestedTag", | ||
60 | value: "example nested tag", | ||
61 | }, | ||
62 | ], | ||
63 | } | ||
64 | ]) | ||
65 | } | ||
66 | }) | ||
67 | } | ||
68 | |||
69 | async function unregister () { | ||
70 | return | ||
71 | } | ||
72 | |||
73 | module.exports = { | ||
74 | register, | ||
75 | unregister | ||
76 | } | ||
77 | |||
78 | // ############################################################################ | ||
79 | |||
80 | function addToCount (obj) { | ||
81 | return Object.assign({}, obj, { count: obj.count + 1 }) | ||
82 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-podcast-custom-tags/package.json b/packages/tests/fixtures/peertube-plugin-test-podcast-custom-tags/package.json new file mode 100644 index 000000000..0f5a05a79 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-podcast-custom-tags/package.json | |||
@@ -0,0 +1,19 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-podcast-custom-tags", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test custom tags in Podcast RSS feeds", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [] | ||
19 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-six/main.js b/packages/tests/fixtures/peertube-plugin-test-six/main.js new file mode 100644 index 000000000..243b041e7 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-six/main.js | |||
@@ -0,0 +1,46 @@ | |||
1 | const fs = require('fs') | ||
2 | const path = require('path') | ||
3 | |||
4 | async function register ({ | ||
5 | storageManager, | ||
6 | peertubeHelpers, | ||
7 | getRouter | ||
8 | }) { | ||
9 | const { logger } = peertubeHelpers | ||
10 | |||
11 | { | ||
12 | await storageManager.storeData('superkey', { value: 'toto' }) | ||
13 | await storageManager.storeData('anotherkey', { value: 'toto2' }) | ||
14 | await storageManager.storeData('storedArrayKey', ['toto', 'toto2']) | ||
15 | |||
16 | const result = await storageManager.getData('superkey') | ||
17 | logger.info('superkey stored value is %s', result.value) | ||
18 | |||
19 | const storedArrayValue = await storageManager.getData('storedArrayKey') | ||
20 | logger.info('storedArrayKey isArray is %s', Array.isArray(storedArrayValue) ? 'true' : 'false') | ||
21 | logger.info('storedArrayKey stored value is %s', storedArrayValue.join(', ')) | ||
22 | } | ||
23 | |||
24 | { | ||
25 | getRouter().get('/create-file', async (req, res) => { | ||
26 | const basePath = peertubeHelpers.plugin.getDataDirectoryPath() | ||
27 | |||
28 | fs.writeFile(path.join(basePath, 'Aladdin.txt'), 'Prince Ali', function (err) { | ||
29 | if (err) return res.sendStatus(500) | ||
30 | |||
31 | res.sendStatus(200) | ||
32 | }) | ||
33 | }) | ||
34 | } | ||
35 | } | ||
36 | |||
37 | async function unregister () { | ||
38 | return | ||
39 | } | ||
40 | |||
41 | module.exports = { | ||
42 | register, | ||
43 | unregister | ||
44 | } | ||
45 | |||
46 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-six/package.json b/packages/tests/fixtures/peertube-plugin-test-six/package.json new file mode 100644 index 000000000..8c97826b0 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-six/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-six", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test 6", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-transcoding-one/main.js b/packages/tests/fixtures/peertube-plugin-test-transcoding-one/main.js new file mode 100644 index 000000000..c4ae777f5 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-transcoding-one/main.js | |||
@@ -0,0 +1,92 @@ | |||
1 | async function register ({ transcodingManager }) { | ||
2 | |||
3 | // Output options | ||
4 | { | ||
5 | { | ||
6 | const builder = () => { | ||
7 | return { | ||
8 | outputOptions: [ | ||
9 | '-r 10' | ||
10 | ] | ||
11 | } | ||
12 | } | ||
13 | |||
14 | transcodingManager.addVODProfile('libx264', 'low-vod', builder) | ||
15 | } | ||
16 | |||
17 | { | ||
18 | const builder = (options) => { | ||
19 | return { | ||
20 | outputOptions: [ | ||
21 | '-r:' + options.streamNum + ' 50' | ||
22 | ] | ||
23 | } | ||
24 | } | ||
25 | |||
26 | transcodingManager.addLiveProfile('libx264', 'high-live', builder) | ||
27 | } | ||
28 | } | ||
29 | |||
30 | // Input options | ||
31 | { | ||
32 | { | ||
33 | const builder = () => { | ||
34 | return { | ||
35 | inputOptions: [ | ||
36 | '-r 5' | ||
37 | ] | ||
38 | } | ||
39 | } | ||
40 | |||
41 | transcodingManager.addVODProfile('libx264', 'input-options-vod', builder) | ||
42 | } | ||
43 | |||
44 | { | ||
45 | const builder = () => { | ||
46 | return { | ||
47 | inputOptions: [ | ||
48 | '-r 50' | ||
49 | ] | ||
50 | } | ||
51 | } | ||
52 | |||
53 | transcodingManager.addLiveProfile('libx264', 'input-options-live', builder) | ||
54 | } | ||
55 | } | ||
56 | |||
57 | // Scale filters | ||
58 | { | ||
59 | { | ||
60 | const builder = () => { | ||
61 | return { | ||
62 | scaleFilter: { | ||
63 | name: 'Glomgold' | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | |||
68 | transcodingManager.addVODProfile('libx264', 'bad-scale-vod', builder) | ||
69 | } | ||
70 | |||
71 | { | ||
72 | const builder = () => { | ||
73 | return { | ||
74 | scaleFilter: { | ||
75 | name: 'Flintheart' | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | |||
80 | transcodingManager.addLiveProfile('libx264', 'bad-scale-live', builder) | ||
81 | } | ||
82 | } | ||
83 | } | ||
84 | |||
85 | async function unregister () { | ||
86 | return | ||
87 | } | ||
88 | |||
89 | module.exports = { | ||
90 | register, | ||
91 | unregister | ||
92 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-transcoding-one/package.json b/packages/tests/fixtures/peertube-plugin-test-transcoding-one/package.json new file mode 100644 index 000000000..bedbfa051 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-transcoding-one/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-transcoding-one", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test transcoding 1", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-transcoding-two/main.js b/packages/tests/fixtures/peertube-plugin-test-transcoding-two/main.js new file mode 100644 index 000000000..a914bce49 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-transcoding-two/main.js | |||
@@ -0,0 +1,38 @@ | |||
1 | async function register ({ transcodingManager }) { | ||
2 | |||
3 | { | ||
4 | const builder = () => { | ||
5 | return { | ||
6 | outputOptions: [] | ||
7 | } | ||
8 | } | ||
9 | |||
10 | transcodingManager.addVODProfile('libopus', 'test-vod-profile', builder) | ||
11 | transcodingManager.addVODProfile('libvpx-vp9', 'test-vod-profile', builder) | ||
12 | |||
13 | transcodingManager.addVODEncoderPriority('audio', 'libopus', 1000) | ||
14 | transcodingManager.addVODEncoderPriority('video', 'libvpx-vp9', 1000) | ||
15 | } | ||
16 | |||
17 | { | ||
18 | const builder = (options) => { | ||
19 | return { | ||
20 | outputOptions: [ | ||
21 | '-b:' + options.streamNum + ' 10K' | ||
22 | ] | ||
23 | } | ||
24 | } | ||
25 | |||
26 | transcodingManager.addLiveProfile('libopus', 'test-live-profile', builder) | ||
27 | transcodingManager.addLiveEncoderPriority('audio', 'libopus', 1000) | ||
28 | } | ||
29 | } | ||
30 | |||
31 | async function unregister () { | ||
32 | return | ||
33 | } | ||
34 | |||
35 | module.exports = { | ||
36 | register, | ||
37 | unregister | ||
38 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-transcoding-two/package.json b/packages/tests/fixtures/peertube-plugin-test-transcoding-two/package.json new file mode 100644 index 000000000..34be0454b --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-transcoding-two/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-transcoding-two", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test transcoding 2", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-unloading/lib.js b/packages/tests/fixtures/peertube-plugin-test-unloading/lib.js new file mode 100644 index 000000000..f57e7cb01 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-unloading/lib.js | |||
@@ -0,0 +1,2 @@ | |||
1 | const d = new Date() | ||
2 | exports.value = d.getTime() | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-unloading/main.js b/packages/tests/fixtures/peertube-plugin-test-unloading/main.js new file mode 100644 index 000000000..5c8457cef --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-unloading/main.js | |||
@@ -0,0 +1,14 @@ | |||
1 | const lib = require('./lib') | ||
2 | |||
3 | async function register ({ getRouter }) { | ||
4 | const router = getRouter() | ||
5 | router.get('/get', (req, res) => res.json({ message: lib.value })) | ||
6 | } | ||
7 | |||
8 | async function unregister () { | ||
9 | } | ||
10 | |||
11 | module.exports = { | ||
12 | register, | ||
13 | unregister | ||
14 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-unloading/package.json b/packages/tests/fixtures/peertube-plugin-test-unloading/package.json new file mode 100644 index 000000000..7076d4b6f --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-unloading/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-unloading", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test (modules unloading)", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-video-constants/main.js b/packages/tests/fixtures/peertube-plugin-test-video-constants/main.js new file mode 100644 index 000000000..06527bd35 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-video-constants/main.js | |||
@@ -0,0 +1,46 @@ | |||
1 | async function register ({ | ||
2 | videoCategoryManager, | ||
3 | videoLicenceManager, | ||
4 | videoLanguageManager, | ||
5 | videoPrivacyManager, | ||
6 | playlistPrivacyManager, | ||
7 | getRouter | ||
8 | }) { | ||
9 | videoLanguageManager.addConstant('al_bhed', 'Al Bhed') | ||
10 | videoLanguageManager.addLanguage('al_bhed2', 'Al Bhed 2') | ||
11 | videoLanguageManager.addConstant('al_bhed3', 'Al Bhed 3') | ||
12 | videoLanguageManager.deleteConstant('en') | ||
13 | videoLanguageManager.deleteLanguage('fr') | ||
14 | videoLanguageManager.deleteConstant('al_bhed3') | ||
15 | |||
16 | videoCategoryManager.addCategory(42, 'Best category') | ||
17 | videoCategoryManager.addConstant(43, 'High best category') | ||
18 | videoCategoryManager.deleteConstant(1) // Music | ||
19 | videoCategoryManager.deleteCategory(2) // Films | ||
20 | |||
21 | videoLicenceManager.addLicence(42, 'Best licence') | ||
22 | videoLicenceManager.addConstant(43, 'High best licence') | ||
23 | videoLicenceManager.deleteConstant(1) // Attribution | ||
24 | videoLicenceManager.deleteConstant(7) // Public domain | ||
25 | |||
26 | videoPrivacyManager.deleteConstant(2) | ||
27 | videoPrivacyManager.deletePrivacy(2) | ||
28 | playlistPrivacyManager.deleteConstant(3) | ||
29 | playlistPrivacyManager.deletePlaylistPrivacy(3) | ||
30 | |||
31 | { | ||
32 | const router = getRouter() | ||
33 | router.get('/reset-categories', (req, res) => { | ||
34 | videoCategoryManager.resetConstants() | ||
35 | |||
36 | res.sendStatus(204) | ||
37 | }) | ||
38 | } | ||
39 | } | ||
40 | |||
41 | async function unregister () {} | ||
42 | |||
43 | module.exports = { | ||
44 | register, | ||
45 | unregister | ||
46 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-video-constants/package.json b/packages/tests/fixtures/peertube-plugin-test-video-constants/package.json new file mode 100644 index 000000000..0fcf39933 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-video-constants/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-video-constants", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test video constants", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-websocket/main.js b/packages/tests/fixtures/peertube-plugin-test-websocket/main.js new file mode 100644 index 000000000..3fde76cfe --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-websocket/main.js | |||
@@ -0,0 +1,36 @@ | |||
1 | const WebSocketServer = require('ws').WebSocketServer | ||
2 | |||
3 | async function register ({ | ||
4 | registerWebSocketRoute | ||
5 | }) { | ||
6 | const wss = new WebSocketServer({ noServer: true }) | ||
7 | |||
8 | wss.on('connection', function connection(ws) { | ||
9 | ws.on('message', function message(data) { | ||
10 | if (data.toString() === 'ping') { | ||
11 | ws.send('pong') | ||
12 | } | ||
13 | }) | ||
14 | }) | ||
15 | |||
16 | registerWebSocketRoute({ | ||
17 | route: '/toto', | ||
18 | |||
19 | handler: (request, socket, head) => { | ||
20 | wss.handleUpgrade(request, socket, head, ws => { | ||
21 | wss.emit('connection', ws, request) | ||
22 | }) | ||
23 | } | ||
24 | }) | ||
25 | } | ||
26 | |||
27 | async function unregister () { | ||
28 | return | ||
29 | } | ||
30 | |||
31 | module.exports = { | ||
32 | register, | ||
33 | unregister | ||
34 | } | ||
35 | |||
36 | // ########################################################################### | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test-websocket/package.json b/packages/tests/fixtures/peertube-plugin-test-websocket/package.json new file mode 100644 index 000000000..89c8baa04 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test-websocket/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-websocket", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test websocket", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test/languages/fr.json b/packages/tests/fixtures/peertube-plugin-test/languages/fr.json new file mode 100644 index 000000000..9e52f7065 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test/languages/fr.json | |||
@@ -0,0 +1,3 @@ | |||
1 | { | ||
2 | "Hi": "Coucou" | ||
3 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test/main.js b/packages/tests/fixtures/peertube-plugin-test/main.js new file mode 100644 index 000000000..e16bf0ca3 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test/main.js | |||
@@ -0,0 +1,477 @@ | |||
1 | async function register ({ registerHook, registerSetting, settingsManager, storageManager, peertubeHelpers }) { | ||
2 | { | ||
3 | const actionHooks = [ | ||
4 | 'action:application.listening', | ||
5 | 'action:notifier.notification.created', | ||
6 | |||
7 | 'action:api.video.updated', | ||
8 | 'action:api.video.deleted', | ||
9 | 'action:api.video.uploaded', | ||
10 | 'action:api.video.viewed', | ||
11 | |||
12 | 'action:api.video.file-updated', | ||
13 | |||
14 | 'action:api.video-channel.created', | ||
15 | 'action:api.video-channel.updated', | ||
16 | 'action:api.video-channel.deleted', | ||
17 | |||
18 | 'action:api.live-video.created', | ||
19 | 'action:live.video.state.updated', | ||
20 | |||
21 | 'action:api.video-thread.created', | ||
22 | 'action:api.video-comment-reply.created', | ||
23 | 'action:api.video-comment.deleted', | ||
24 | |||
25 | 'action:api.video-caption.created', | ||
26 | 'action:api.video-caption.deleted', | ||
27 | |||
28 | 'action:api.user.blocked', | ||
29 | 'action:api.user.unblocked', | ||
30 | 'action:api.user.registered', | ||
31 | 'action:api.user.created', | ||
32 | 'action:api.user.deleted', | ||
33 | 'action:api.user.updated', | ||
34 | 'action:api.user.oauth2-got-token', | ||
35 | |||
36 | 'action:api.video-playlist-element.created' | ||
37 | ] | ||
38 | |||
39 | for (const h of actionHooks) { | ||
40 | registerHook({ | ||
41 | target: h, | ||
42 | handler: () => peertubeHelpers.logger.debug('Run hook %s.', h) | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) { | ||
47 | registerHook({ | ||
48 | target: h, | ||
49 | handler: ({ video, videoAPObject }) => { | ||
50 | peertubeHelpers.logger.debug('Run hook %s - AP %s - video %s.', h, video.name, videoAPObject.name ) | ||
51 | } | ||
52 | }) | ||
53 | } | ||
54 | } | ||
55 | |||
56 | registerHook({ | ||
57 | target: 'filter:api.videos.list.params', | ||
58 | handler: obj => addToCount(obj) | ||
59 | }) | ||
60 | |||
61 | registerHook({ | ||
62 | target: 'filter:api.videos.list.result', | ||
63 | handler: obj => addToTotal(obj) | ||
64 | }) | ||
65 | |||
66 | registerHook({ | ||
67 | target: 'filter:api.video-playlist.videos.list.params', | ||
68 | handler: obj => addToCount(obj) | ||
69 | }) | ||
70 | |||
71 | registerHook({ | ||
72 | target: 'filter:api.video-playlist.videos.list.result', | ||
73 | handler: obj => addToTotal(obj) | ||
74 | }) | ||
75 | |||
76 | registerHook({ | ||
77 | target: 'filter:api.accounts.videos.list.params', | ||
78 | handler: obj => addToCount(obj) | ||
79 | }) | ||
80 | |||
81 | registerHook({ | ||
82 | target: 'filter:api.accounts.videos.list.result', | ||
83 | handler: obj => addToTotal(obj, 2) | ||
84 | }) | ||
85 | |||
86 | registerHook({ | ||
87 | target: 'filter:api.video-channels.videos.list.params', | ||
88 | handler: obj => addToCount(obj, 3) | ||
89 | }) | ||
90 | |||
91 | registerHook({ | ||
92 | target: 'filter:api.video-channels.videos.list.result', | ||
93 | handler: obj => addToTotal(obj, 3) | ||
94 | }) | ||
95 | |||
96 | registerHook({ | ||
97 | target: 'filter:api.user.me.videos.list.params', | ||
98 | handler: obj => addToCount(obj, 4) | ||
99 | }) | ||
100 | |||
101 | registerHook({ | ||
102 | target: 'filter:api.user.me.videos.list.result', | ||
103 | handler: obj => addToTotal(obj, 4) | ||
104 | }) | ||
105 | |||
106 | registerHook({ | ||
107 | target: 'filter:api.user.me.subscription-videos.list.params', | ||
108 | handler: obj => addToCount(obj) | ||
109 | }) | ||
110 | |||
111 | registerHook({ | ||
112 | target: 'filter:api.user.me.subscription-videos.list.result', | ||
113 | handler: obj => addToTotal(obj, 4) | ||
114 | }) | ||
115 | |||
116 | registerHook({ | ||
117 | target: 'filter:api.video.get.result', | ||
118 | handler: video => { | ||
119 | video.name += ' <3' | ||
120 | |||
121 | return video | ||
122 | } | ||
123 | }) | ||
124 | |||
125 | // --------------------------------------------------------------------------- | ||
126 | |||
127 | registerHook({ | ||
128 | target: 'filter:api.video-channels.list.params', | ||
129 | handler: obj => addToCount(obj, 1) | ||
130 | }) | ||
131 | |||
132 | registerHook({ | ||
133 | target: 'filter:api.video-channels.list.result', | ||
134 | handler: obj => addToTotal(obj, 1) | ||
135 | }) | ||
136 | |||
137 | registerHook({ | ||
138 | target: 'filter:api.video-channel.get.result', | ||
139 | handler: channel => { | ||
140 | channel.name += ' <3' | ||
141 | |||
142 | return channel | ||
143 | } | ||
144 | }) | ||
145 | |||
146 | // --------------------------------------------------------------------------- | ||
147 | |||
148 | for (const hook of [ 'filter:api.video.upload.accept.result', 'filter:api.live-video.create.accept.result' ]) { | ||
149 | registerHook({ | ||
150 | target: hook, | ||
151 | handler: ({ accepted }, { videoBody, liveVideoBody }) => { | ||
152 | if (!accepted) return { accepted: false } | ||
153 | |||
154 | const name = videoBody | ||
155 | ? videoBody.name | ||
156 | : liveVideoBody.name | ||
157 | |||
158 | if (name.indexOf('bad word') !== -1) return { accepted: false, errorMessage: 'bad word' } | ||
159 | |||
160 | return { accepted: true } | ||
161 | } | ||
162 | }) | ||
163 | } | ||
164 | |||
165 | registerHook({ | ||
166 | target: 'filter:api.video.update-file.accept.result', | ||
167 | handler: ({ accepted }, { videoFile }) => { | ||
168 | if (!accepted) return { accepted: false } | ||
169 | if (videoFile.filename.includes('webm')) return { accepted: false, errorMessage: 'no webm' } | ||
170 | |||
171 | return { accepted: true } | ||
172 | } | ||
173 | }) | ||
174 | |||
175 | registerHook({ | ||
176 | target: 'filter:api.video.pre-import-url.accept.result', | ||
177 | handler: ({ accepted }, { videoImportBody }) => { | ||
178 | if (!accepted) return { accepted: false } | ||
179 | if (videoImportBody.targetUrl.includes('bad')) return { accepted: false, errorMessage: 'bad target url' } | ||
180 | |||
181 | return { accepted: true } | ||
182 | } | ||
183 | }) | ||
184 | |||
185 | registerHook({ | ||
186 | target: 'filter:api.video.pre-import-torrent.accept.result', | ||
187 | handler: ({ accepted }, { videoImportBody }) => { | ||
188 | if (!accepted) return { accepted: false } | ||
189 | if (videoImportBody.name.includes('bad torrent')) return { accepted: false, errorMessage: 'bad torrent' } | ||
190 | |||
191 | return { accepted: true } | ||
192 | } | ||
193 | }) | ||
194 | |||
195 | registerHook({ | ||
196 | target: 'filter:api.video.post-import-url.accept.result', | ||
197 | handler: ({ accepted }, { video }) => { | ||
198 | if (!accepted) return { accepted: false } | ||
199 | if (video.name.includes('bad word')) return { accepted: false, errorMessage: 'bad word' } | ||
200 | |||
201 | return { accepted: true } | ||
202 | } | ||
203 | }) | ||
204 | |||
205 | registerHook({ | ||
206 | target: 'filter:api.video.post-import-torrent.accept.result', | ||
207 | handler: ({ accepted }, { video }) => { | ||
208 | if (!accepted) return { accepted: false } | ||
209 | if (video.name.includes('bad word')) return { accepted: false, errorMessage: 'bad word' } | ||
210 | |||
211 | return { accepted: true } | ||
212 | } | ||
213 | }) | ||
214 | |||
215 | // --------------------------------------------------------------------------- | ||
216 | |||
217 | registerHook({ | ||
218 | target: 'filter:api.video-thread.create.accept.result', | ||
219 | handler: ({ accepted }, { commentBody }) => checkCommentBadWord(accepted, commentBody) | ||
220 | }) | ||
221 | |||
222 | registerHook({ | ||
223 | target: 'filter:api.video-comment-reply.create.accept.result', | ||
224 | handler: ({ accepted }, { commentBody }) => checkCommentBadWord(accepted, commentBody) | ||
225 | }) | ||
226 | |||
227 | registerHook({ | ||
228 | target: 'filter:activity-pub.remote-video-comment.create.accept.result', | ||
229 | handler: ({ accepted }, { comment }) => checkCommentBadWord(accepted, comment) | ||
230 | }) | ||
231 | |||
232 | // --------------------------------------------------------------------------- | ||
233 | |||
234 | registerHook({ | ||
235 | target: 'filter:activity-pub.activity.context.build.result', | ||
236 | handler: context => context.concat([ { recordedAt: 'https://schema.org/recordedAt' } ]) | ||
237 | }) | ||
238 | |||
239 | registerHook({ | ||
240 | target: 'filter:activity-pub.video.json-ld.build.result', | ||
241 | handler: (jsonld, { video }) => ({ ...jsonld, videoName: video.name }) | ||
242 | }) | ||
243 | |||
244 | // --------------------------------------------------------------------------- | ||
245 | |||
246 | registerHook({ | ||
247 | target: 'filter:api.video-threads.list.params', | ||
248 | handler: obj => addToCount(obj) | ||
249 | }) | ||
250 | |||
251 | registerHook({ | ||
252 | target: 'filter:api.video-threads.list.result', | ||
253 | handler: obj => addToTotal(obj) | ||
254 | }) | ||
255 | |||
256 | registerHook({ | ||
257 | target: 'filter:api.video-thread-comments.list.result', | ||
258 | handler: obj => { | ||
259 | obj.data.forEach(c => c.text += ' <3') | ||
260 | |||
261 | return obj | ||
262 | } | ||
263 | }) | ||
264 | |||
265 | registerHook({ | ||
266 | target: 'filter:video.auto-blacklist.result', | ||
267 | handler: (blacklisted, { video }) => { | ||
268 | if (blacklisted) return true | ||
269 | if (video.name.includes('please blacklist me')) return true | ||
270 | |||
271 | return false | ||
272 | } | ||
273 | }) | ||
274 | |||
275 | { | ||
276 | registerHook({ | ||
277 | target: 'filter:api.user.signup.allowed.result', | ||
278 | handler: (result, params) => { | ||
279 | if (params && params.body && params.body.email && params.body.email.includes('jma 1')) { | ||
280 | return { allowed: false, errorMessage: 'No jma 1' } | ||
281 | } | ||
282 | |||
283 | return result | ||
284 | } | ||
285 | }) | ||
286 | |||
287 | registerHook({ | ||
288 | target: 'filter:api.user.request-signup.allowed.result', | ||
289 | handler: (result, params) => { | ||
290 | if (params && params.body && params.body.email && params.body.email.includes('jma 2')) { | ||
291 | return { allowed: false, errorMessage: 'No jma 2' } | ||
292 | } | ||
293 | |||
294 | return result | ||
295 | } | ||
296 | }) | ||
297 | } | ||
298 | |||
299 | registerHook({ | ||
300 | target: 'filter:api.download.torrent.allowed.result', | ||
301 | handler: (result, params) => { | ||
302 | if (params && params.downloadName.includes('bad torrent')) { | ||
303 | return { allowed: false, errorMessage: 'Liu Bei' } | ||
304 | } | ||
305 | |||
306 | return result | ||
307 | } | ||
308 | }) | ||
309 | |||
310 | registerHook({ | ||
311 | target: 'filter:api.download.video.allowed.result', | ||
312 | handler: async (result, params) => { | ||
313 | const loggedInUser = await peertubeHelpers.user.getAuthUser(params.res) | ||
314 | if (loggedInUser) return { allowed: true } | ||
315 | |||
316 | if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) { | ||
317 | return { allowed: false, errorMessage: 'Cao Cao' } | ||
318 | } | ||
319 | |||
320 | if (params && params.streamingPlaylist && params.video.name.includes('bad playlist file')) { | ||
321 | return { allowed: false, errorMessage: 'Sun Jian' } | ||
322 | } | ||
323 | |||
324 | return result | ||
325 | } | ||
326 | }) | ||
327 | |||
328 | // --------------------------------------------------------------------------- | ||
329 | |||
330 | registerHook({ | ||
331 | target: 'filter:html.embed.video.allowed.result', | ||
332 | handler: (result, params) => { | ||
333 | return { | ||
334 | allowed: false, | ||
335 | html: 'Lu Bu' | ||
336 | } | ||
337 | } | ||
338 | }) | ||
339 | |||
340 | registerHook({ | ||
341 | target: 'filter:html.embed.video-playlist.allowed.result', | ||
342 | handler: (result, params) => { | ||
343 | return { | ||
344 | allowed: false, | ||
345 | html: 'Diao Chan' | ||
346 | } | ||
347 | } | ||
348 | }) | ||
349 | |||
350 | // --------------------------------------------------------------------------- | ||
351 | |||
352 | registerHook({ | ||
353 | target: 'filter:html.client.json-ld.result', | ||
354 | handler: (jsonld, context) => { | ||
355 | if (!context || !context.video) return jsonld | ||
356 | |||
357 | return Object.assign(jsonld, { recordedAt: 'http://example.com/recordedAt' }) | ||
358 | } | ||
359 | }) | ||
360 | |||
361 | // --------------------------------------------------------------------------- | ||
362 | |||
363 | registerHook({ | ||
364 | target: 'filter:api.server.stats.get.result', | ||
365 | handler: (result) => { | ||
366 | return { ...result, customStats: 14 } | ||
367 | } | ||
368 | }) | ||
369 | |||
370 | registerHook({ | ||
371 | target: 'filter:job-queue.process.params', | ||
372 | handler: (object, context) => { | ||
373 | if (context.type !== 'video-studio-edition') return object | ||
374 | |||
375 | object.data.tasks = [ | ||
376 | { | ||
377 | name: 'cut', | ||
378 | options: { | ||
379 | start: 0, | ||
380 | end: 1 | ||
381 | } | ||
382 | } | ||
383 | ] | ||
384 | |||
385 | return object | ||
386 | } | ||
387 | }) | ||
388 | |||
389 | registerHook({ | ||
390 | target: 'filter:transcoding.auto.resolutions-to-transcode.result', | ||
391 | handler: (object, context) => { | ||
392 | if (context.video.name.includes('transcode-filter')) { | ||
393 | object = [ 100 ] | ||
394 | } | ||
395 | |||
396 | return object | ||
397 | } | ||
398 | }) | ||
399 | |||
400 | // Upload/import/live attributes | ||
401 | for (const target of [ | ||
402 | 'filter:api.video.upload.video-attribute.result', | ||
403 | 'filter:api.video.import-url.video-attribute.result', | ||
404 | 'filter:api.video.import-torrent.video-attribute.result', | ||
405 | 'filter:api.video.live.video-attribute.result' | ||
406 | ]) { | ||
407 | registerHook({ | ||
408 | target, | ||
409 | handler: (result) => { | ||
410 | return { ...result, description: result.description + ' - ' + target } | ||
411 | } | ||
412 | }) | ||
413 | } | ||
414 | |||
415 | { | ||
416 | const filterHooks = [ | ||
417 | 'filter:api.search.videos.local.list.params', | ||
418 | 'filter:api.search.videos.local.list.result', | ||
419 | 'filter:api.search.videos.index.list.params', | ||
420 | 'filter:api.search.videos.index.list.result', | ||
421 | 'filter:api.search.video-channels.local.list.params', | ||
422 | 'filter:api.search.video-channels.local.list.result', | ||
423 | 'filter:api.search.video-channels.index.list.params', | ||
424 | 'filter:api.search.video-channels.index.list.result', | ||
425 | 'filter:api.search.video-playlists.local.list.params', | ||
426 | 'filter:api.search.video-playlists.local.list.result', | ||
427 | 'filter:api.search.video-playlists.index.list.params', | ||
428 | 'filter:api.search.video-playlists.index.list.result', | ||
429 | |||
430 | 'filter:api.overviews.videos.list.params', | ||
431 | 'filter:api.overviews.videos.list.result', | ||
432 | |||
433 | 'filter:job-queue.process.params', | ||
434 | 'filter:job-queue.process.result' | ||
435 | ] | ||
436 | |||
437 | for (const h of filterHooks) { | ||
438 | registerHook({ | ||
439 | target: h, | ||
440 | handler: (obj) => { | ||
441 | peertubeHelpers.logger.debug('Run hook %s.', h) | ||
442 | |||
443 | return obj | ||
444 | } | ||
445 | }) | ||
446 | } | ||
447 | } | ||
448 | } | ||
449 | |||
450 | async function unregister () { | ||
451 | return | ||
452 | } | ||
453 | |||
454 | module.exports = { | ||
455 | register, | ||
456 | unregister | ||
457 | } | ||
458 | |||
459 | // ############################################################################ | ||
460 | |||
461 | function addToCount (obj, amount = 1) { | ||
462 | return Object.assign({}, obj, { count: obj.count + amount }) | ||
463 | } | ||
464 | |||
465 | function addToTotal (result, amount = 1) { | ||
466 | return { | ||
467 | data: result.data, | ||
468 | total: result.total + amount | ||
469 | } | ||
470 | } | ||
471 | |||
472 | function checkCommentBadWord (accepted, commentBody) { | ||
473 | if (!accepted) return { accepted: false } | ||
474 | if (commentBody.text.indexOf('bad word') !== -1) return { accepted: false, errorMessage: 'bad word '} | ||
475 | |||
476 | return { accepted: true } | ||
477 | } | ||
diff --git a/packages/tests/fixtures/peertube-plugin-test/package.json b/packages/tests/fixtures/peertube-plugin-test/package.json new file mode 100644 index 000000000..108f21fd6 --- /dev/null +++ b/packages/tests/fixtures/peertube-plugin-test/package.json | |||
@@ -0,0 +1,22 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": { | ||
20 | "fr-FR": "./languages/fr.json" | ||
21 | } | ||
22 | } | ||
diff --git a/packages/tests/fixtures/rtmps.cert b/packages/tests/fixtures/rtmps.cert new file mode 100644 index 000000000..3ef606c52 --- /dev/null +++ b/packages/tests/fixtures/rtmps.cert | |||
@@ -0,0 +1,21 @@ | |||
1 | -----BEGIN CERTIFICATE----- | ||
2 | MIIDazCCAlOgAwIBAgIUKNycLAZUs2jFsWUW+zZhBkpLB2wwDQYJKoZIhvcNAQEL | ||
3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM | ||
4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTExMDUxMDA4MzhaFw0yMTEy | ||
5 | MDUxMDA4MzhaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw | ||
6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB | ||
7 | AQUAA4IBDwAwggEKAoIBAQDak20d81KG/9mVLU6Qw/uRniC935yf9Rlp8FVCDxUd | ||
8 | zLbfHjrnIOv8kqinUI0nuEQC4DnF7Rbafe88WDU33Q8ixU/R0czUGq1AEwIjyN30 | ||
9 | 5NjokCb26xWIly7RCfc/Ot6tjguHwKvcxqJMNC0Lit9Go9MDVnGFLkgHia68P72T | ||
10 | ZDVV44YpzwYDicwQs5C4nZ4yzAeclia07qfUY0VAEZlxJ/9zjwYHCT0AKaEPH35E | ||
11 | dUvjuvJ1OSHSN1S4acR+TPR3FwKQh3H/M/GWIqoiIOpdjFUBLs80QOM2aNrLmlyP | ||
12 | JtyFJLxCP7Ery9fGY/yzHeSxpgOKwZopD6uHZKi5yazNAgMBAAGjUzBRMB0GA1Ud | ||
13 | DgQWBBSSjhRQdWsybNQMLMhkwV+xiP2uoDAfBgNVHSMEGDAWgBSSjhRQdWsybNQM | ||
14 | LMhkwV+xiP2uoDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC8 | ||
15 | rJu3J5sqVKNQaXOmLPd49RM7KG3Y1KPqbQi1lh+sW6aefZ9daeh3JDYGBZGPG/Fi | ||
16 | IMMP+LhGG0WqDm4ClK00wyNhBuNPEyzvuN/WMRX5djPxO1IZi+KogFwXsn853Ov9 | ||
17 | oV3nxArNNjDu2n92FiB7RTlXRXPIoRo2zEBcLvveGySn9XUazRzlqx6FAxYe2xsw | ||
18 | U3cZ6/wwU1YsEZa5bwIQk+gkFj3zDsTyEkn2ntcE2NlR+AhCHKa/yAxgPFycAVPX | ||
19 | 2o+wNnc6H4syP98mMGj9hEE3RSJyCPgGBlgi7Swl64G3YygFPJzfLX9YTuxwr/eI | ||
20 | oitEjF9ljtmdEnf0RdOj | ||
21 | -----END CERTIFICATE----- | ||
diff --git a/packages/tests/fixtures/rtmps.key b/packages/tests/fixtures/rtmps.key new file mode 100644 index 000000000..14a85e70a --- /dev/null +++ b/packages/tests/fixtures/rtmps.key | |||
@@ -0,0 +1,28 @@ | |||
1 | -----BEGIN PRIVATE KEY----- | ||
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDak20d81KG/9mV | ||
3 | LU6Qw/uRniC935yf9Rlp8FVCDxUdzLbfHjrnIOv8kqinUI0nuEQC4DnF7Rbafe88 | ||
4 | WDU33Q8ixU/R0czUGq1AEwIjyN305NjokCb26xWIly7RCfc/Ot6tjguHwKvcxqJM | ||
5 | NC0Lit9Go9MDVnGFLkgHia68P72TZDVV44YpzwYDicwQs5C4nZ4yzAeclia07qfU | ||
6 | Y0VAEZlxJ/9zjwYHCT0AKaEPH35EdUvjuvJ1OSHSN1S4acR+TPR3FwKQh3H/M/GW | ||
7 | IqoiIOpdjFUBLs80QOM2aNrLmlyPJtyFJLxCP7Ery9fGY/yzHeSxpgOKwZopD6uH | ||
8 | ZKi5yazNAgMBAAECggEAND7C+UK8+jnTl13CBsZhrnfemaQGexGJ5pGkv2p9gKb7 | ||
9 | Gy/Nooty/OdNWtjdNJ5N22YfSRkXulgZxBHNfrHfOU9yedOtIxHRUZx5iXYs36mH | ||
10 | 02cJeUHN3t1MOnkoWTvIGDH4vZUnP1lXV+Gs1rJ2Fht4h7a04cGjQ/H8C1EtDjqX | ||
11 | kzH2T/gwo5hdGrxifRTs5wCVoP/iUwNtBI4WrY2rfC6sV+NOICgp0xX0NvGWZ8UT | ||
12 | K1Ntpl8IxnxmeBd26d+Gbjc9d9fIRDtyXby4YOIlDZxnIiZEI0I452JqGl/jrXaP | ||
13 | F3Troet4OBj5uH5s374d6ubKq66XogiLMIjEj2tYfQKBgQDtuaOu+y549bFJKVc9 | ||
14 | TCiWSOl/0j2kKKG8UG23zMC//AT13WqZDT5ObfOAuMhy70au/PD84D9RU/+gRVWb | ||
15 | ptfybD9ugRNC8PkmdT82uYtZpS4+Xw4qyWVRgqQFmjSYz63cLcULVi8kiG8XmG5u | ||
16 | QGgT/tNv5mxhOMUGSxhClOpLBwKBgQDrYO9UrLs+gDVKbHF4Dh+YJpaLnwwF+TFA | ||
17 | j3ZbkE0XEeeXp/YDgyClmWwEkteJeNljtreCZ9gMkx3JdR9i8uecUQ2tFDBg3cN0 | ||
18 | BZAex2jFwSb0QbfzHNnE07I+aEIfHHjYXjzABl+1Yt95giKjce0Ke+8Zzahue0+9 | ||
19 | lYcAHemQiwKBgQCs9JAbIdJo3NBUW0iGZ19sH7YKciq4wXsSaC27OLPPugrd2m7Q | ||
20 | 1arMIwCzWT01KdLyQ0MNqBVJFWT49RjYuuWIEauAuVYLMQkEKu+H4Cx7V0syw7Op | ||
21 | +4bEa9jr3op/1zE17PLcUaLQ4JZ6w0Ms4Z0XVyH72thlT4lBD+ehoXhohwKBgEtJ | ||
22 | LAPnY9Sv6Vuup/SAf/aIkSqDarMWa3x85pyO4Tl5zpuha3zgGjcdhYFI/ovIDbBp | ||
23 | JvUdBeuvup1PSwS5MP+8pSUxCfBRvkyD4v8VRRvLlgwWYSHvnm/oTmDLtCqDTtvV | ||
24 | +JRq9X3s7BHPYAjrTahGz8lvEGqWIoE/LHkLGEPVAoGAaF3VHuqDfmD9PJUAlsU1 | ||
25 | qxN7yfOd2ve0+66Ghus24DVqUFqwp5f2AxZXYUtSaNUp8fVbqIi+Yq3YDTU2KfId | ||
26 | 5QNA/AiKi4VUNLElsG5DZlbszsE5KNp9fWQoggdQ5LND7AGEKeFERHOVQ7C5sc/C | ||
27 | omIqK5/PsZmaf4OZLyecxJY= | ||
28 | -----END PRIVATE KEY----- | ||
diff --git a/packages/tests/fixtures/sample.ogg b/packages/tests/fixtures/sample.ogg new file mode 100644 index 000000000..0d7f43eb7 --- /dev/null +++ b/packages/tests/fixtures/sample.ogg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/subtitle-bad.txt b/packages/tests/fixtures/subtitle-bad.txt new file mode 100644 index 000000000..a2a30ae47 --- /dev/null +++ b/packages/tests/fixtures/subtitle-bad.txt | |||
@@ -0,0 +1,11 @@ | |||
1 | 1 | ||
2 | 00:00:01,600 --> 00:00:04,200 | ||
3 | English (US) | ||
4 | |||
5 | 2 | ||
6 | 00:00:05,900 --> 00:00:07,999 | ||
7 | This is a subtitle in American English | ||
8 | |||
9 | 3 | ||
10 | 00:00:10,000 --> 00:00:14,000 | ||
11 | Adding subtitles is very easy to do \ No newline at end of file | ||
diff --git a/packages/tests/fixtures/subtitle-good.srt b/packages/tests/fixtures/subtitle-good.srt new file mode 100644 index 000000000..a2a30ae47 --- /dev/null +++ b/packages/tests/fixtures/subtitle-good.srt | |||
@@ -0,0 +1,11 @@ | |||
1 | 1 | ||
2 | 00:00:01,600 --> 00:00:04,200 | ||
3 | English (US) | ||
4 | |||
5 | 2 | ||
6 | 00:00:05,900 --> 00:00:07,999 | ||
7 | This is a subtitle in American English | ||
8 | |||
9 | 3 | ||
10 | 00:00:10,000 --> 00:00:14,000 | ||
11 | Adding subtitles is very easy to do \ No newline at end of file | ||
diff --git a/packages/tests/fixtures/subtitle-good1.vtt b/packages/tests/fixtures/subtitle-good1.vtt new file mode 100644 index 000000000..04cd23946 --- /dev/null +++ b/packages/tests/fixtures/subtitle-good1.vtt | |||
@@ -0,0 +1,8 @@ | |||
1 | WEBVTT | ||
2 | |||
3 | 00:01.000 --> 00:04.000 | ||
4 | Subtitle good 1. | ||
5 | |||
6 | 00:05.000 --> 00:09.000 | ||
7 | - It will perforate your stomach. | ||
8 | - You could die. \ No newline at end of file | ||
diff --git a/packages/tests/fixtures/subtitle-good2.vtt b/packages/tests/fixtures/subtitle-good2.vtt new file mode 100644 index 000000000..4d3256def --- /dev/null +++ b/packages/tests/fixtures/subtitle-good2.vtt | |||
@@ -0,0 +1,8 @@ | |||
1 | WEBVTT | ||
2 | |||
3 | 00:01.000 --> 00:04.000 | ||
4 | Subtitle good 2. | ||
5 | |||
6 | 00:05.000 --> 00:09.000 | ||
7 | - It will perforate your stomach. | ||
8 | - You could die. \ No newline at end of file | ||
diff --git a/packages/tests/fixtures/thumbnail-playlist.jpg b/packages/tests/fixtures/thumbnail-playlist.jpg new file mode 100644 index 000000000..12de5817b --- /dev/null +++ b/packages/tests/fixtures/thumbnail-playlist.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video-720p.torrent b/packages/tests/fixtures/video-720p.torrent new file mode 100644 index 000000000..64bfd5220 --- /dev/null +++ b/packages/tests/fixtures/video-720p.torrent | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_import_preview.jpg b/packages/tests/fixtures/video_import_preview.jpg new file mode 100644 index 000000000..a98da178f --- /dev/null +++ b/packages/tests/fixtures/video_import_preview.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_import_preview_yt_dlp.jpg b/packages/tests/fixtures/video_import_preview_yt_dlp.jpg new file mode 100644 index 000000000..9e8833bf9 --- /dev/null +++ b/packages/tests/fixtures/video_import_preview_yt_dlp.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_import_thumbnail.jpg b/packages/tests/fixtures/video_import_thumbnail.jpg new file mode 100644 index 000000000..9ee1bc382 --- /dev/null +++ b/packages/tests/fixtures/video_import_thumbnail.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_import_thumbnail_yt_dlp.jpg b/packages/tests/fixtures/video_import_thumbnail_yt_dlp.jpg new file mode 100644 index 000000000..a10e07207 --- /dev/null +++ b/packages/tests/fixtures/video_import_thumbnail_yt_dlp.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short.avi b/packages/tests/fixtures/video_short.avi new file mode 100644 index 000000000..88979cab2 --- /dev/null +++ b/packages/tests/fixtures/video_short.avi | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short.mkv b/packages/tests/fixtures/video_short.mkv new file mode 100644 index 000000000..a67f4f806 --- /dev/null +++ b/packages/tests/fixtures/video_short.mkv | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short.mp4 b/packages/tests/fixtures/video_short.mp4 new file mode 100644 index 000000000..35678362b --- /dev/null +++ b/packages/tests/fixtures/video_short.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short.mp4.jpg b/packages/tests/fixtures/video_short.mp4.jpg new file mode 100644 index 000000000..7ac29122c --- /dev/null +++ b/packages/tests/fixtures/video_short.mp4.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short.ogv b/packages/tests/fixtures/video_short.ogv new file mode 100644 index 000000000..9e253da82 --- /dev/null +++ b/packages/tests/fixtures/video_short.ogv | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short.ogv.jpg b/packages/tests/fixtures/video_short.ogv.jpg new file mode 100644 index 000000000..5bc63969b --- /dev/null +++ b/packages/tests/fixtures/video_short.ogv.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short.webm b/packages/tests/fixtures/video_short.webm new file mode 100644 index 000000000..bf4b0ab6c --- /dev/null +++ b/packages/tests/fixtures/video_short.webm | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short.webm.jpg b/packages/tests/fixtures/video_short.webm.jpg new file mode 100644 index 000000000..7ac29122c --- /dev/null +++ b/packages/tests/fixtures/video_short.webm.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short1-preview.webm.jpg b/packages/tests/fixtures/video_short1-preview.webm.jpg new file mode 100644 index 000000000..15454942d --- /dev/null +++ b/packages/tests/fixtures/video_short1-preview.webm.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short1.webm b/packages/tests/fixtures/video_short1.webm new file mode 100644 index 000000000..70ac0c644 --- /dev/null +++ b/packages/tests/fixtures/video_short1.webm | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short1.webm.jpg b/packages/tests/fixtures/video_short1.webm.jpg new file mode 100644 index 000000000..b2740d73d --- /dev/null +++ b/packages/tests/fixtures/video_short1.webm.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short2.webm b/packages/tests/fixtures/video_short2.webm new file mode 100644 index 000000000..13d72dff7 --- /dev/null +++ b/packages/tests/fixtures/video_short2.webm | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short2.webm.jpg b/packages/tests/fixtures/video_short2.webm.jpg new file mode 100644 index 000000000..afe476c7f --- /dev/null +++ b/packages/tests/fixtures/video_short2.webm.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short3.webm b/packages/tests/fixtures/video_short3.webm new file mode 100644 index 000000000..cde5dcd58 --- /dev/null +++ b/packages/tests/fixtures/video_short3.webm | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short3.webm.jpg b/packages/tests/fixtures/video_short3.webm.jpg new file mode 100644 index 000000000..b572f676e --- /dev/null +++ b/packages/tests/fixtures/video_short3.webm.jpg | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_0p.mp4 b/packages/tests/fixtures/video_short_0p.mp4 new file mode 100644 index 000000000..2069a49b8 --- /dev/null +++ b/packages/tests/fixtures/video_short_0p.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_144p.m3u8 b/packages/tests/fixtures/video_short_144p.m3u8 new file mode 100644 index 000000000..96568625b --- /dev/null +++ b/packages/tests/fixtures/video_short_144p.m3u8 | |||
@@ -0,0 +1,13 @@ | |||
1 | #EXTM3U | ||
2 | #EXT-X-VERSION:7 | ||
3 | #EXT-X-TARGETDURATION:4 | ||
4 | #EXT-X-MEDIA-SEQUENCE:0 | ||
5 | #EXT-X-PLAYLIST-TYPE:VOD | ||
6 | #EXT-X-MAP:URI="3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4",BYTERANGE="1375@0" | ||
7 | #EXTINF:4.000000, | ||
8 | #EXT-X-BYTERANGE:10518@1375 | ||
9 | 3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4 | ||
10 | #EXTINF:1.000000, | ||
11 | #EXT-X-BYTERANGE:3741@11893 | ||
12 | 3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4 | ||
13 | #EXT-X-ENDLIST | ||
diff --git a/packages/tests/fixtures/video_short_144p.mp4 b/packages/tests/fixtures/video_short_144p.mp4 new file mode 100644 index 000000000..047d43c17 --- /dev/null +++ b/packages/tests/fixtures/video_short_144p.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_240p.m3u8 b/packages/tests/fixtures/video_short_240p.m3u8 new file mode 100644 index 000000000..96568625b --- /dev/null +++ b/packages/tests/fixtures/video_short_240p.m3u8 | |||
@@ -0,0 +1,13 @@ | |||
1 | #EXTM3U | ||
2 | #EXT-X-VERSION:7 | ||
3 | #EXT-X-TARGETDURATION:4 | ||
4 | #EXT-X-MEDIA-SEQUENCE:0 | ||
5 | #EXT-X-PLAYLIST-TYPE:VOD | ||
6 | #EXT-X-MAP:URI="3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4",BYTERANGE="1375@0" | ||
7 | #EXTINF:4.000000, | ||
8 | #EXT-X-BYTERANGE:10518@1375 | ||
9 | 3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4 | ||
10 | #EXTINF:1.000000, | ||
11 | #EXT-X-BYTERANGE:3741@11893 | ||
12 | 3dd13e27-1ae1-441c-9b77-48c6b95603be-144-fragmented.mp4 | ||
13 | #EXT-X-ENDLIST | ||
diff --git a/packages/tests/fixtures/video_short_240p.mp4 b/packages/tests/fixtures/video_short_240p.mp4 new file mode 100644 index 000000000..46609e81a --- /dev/null +++ b/packages/tests/fixtures/video_short_240p.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_360p.m3u8 b/packages/tests/fixtures/video_short_360p.m3u8 new file mode 100644 index 000000000..f7072dc6d --- /dev/null +++ b/packages/tests/fixtures/video_short_360p.m3u8 | |||
@@ -0,0 +1,13 @@ | |||
1 | #EXTM3U | ||
2 | #EXT-X-VERSION:7 | ||
3 | #EXT-X-TARGETDURATION:4 | ||
4 | #EXT-X-MEDIA-SEQUENCE:0 | ||
5 | #EXT-X-PLAYLIST-TYPE:VOD | ||
6 | #EXT-X-MAP:URI="05c40acd-3e94-4d25-ade8-97f7ff2cf0ac-360-fragmented.mp4",BYTERANGE="1376@0" | ||
7 | #EXTINF:4.000000, | ||
8 | #EXT-X-BYTERANGE:19987@1376 | ||
9 | 05c40acd-3e94-4d25-ade8-97f7ff2cf0ac-360-fragmented.mp4 | ||
10 | #EXTINF:1.000000, | ||
11 | #EXT-X-BYTERANGE:9147@21363 | ||
12 | 05c40acd-3e94-4d25-ade8-97f7ff2cf0ac-360-fragmented.mp4 | ||
13 | #EXT-X-ENDLIST | ||
diff --git a/packages/tests/fixtures/video_short_360p.mp4 b/packages/tests/fixtures/video_short_360p.mp4 new file mode 100644 index 000000000..7a8189bbc --- /dev/null +++ b/packages/tests/fixtures/video_short_360p.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_480.webm b/packages/tests/fixtures/video_short_480.webm new file mode 100644 index 000000000..3145105e1 --- /dev/null +++ b/packages/tests/fixtures/video_short_480.webm | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_480p.m3u8 b/packages/tests/fixtures/video_short_480p.m3u8 new file mode 100644 index 000000000..5ff30dfa7 --- /dev/null +++ b/packages/tests/fixtures/video_short_480p.m3u8 | |||
@@ -0,0 +1,13 @@ | |||
1 | #EXTM3U | ||
2 | #EXT-X-VERSION:7 | ||
3 | #EXT-X-TARGETDURATION:4 | ||
4 | #EXT-X-MEDIA-SEQUENCE:0 | ||
5 | #EXT-X-PLAYLIST-TYPE:VOD | ||
6 | #EXT-X-MAP:URI="f9377e69-d8f2-4de8-8087-ddbca6629829-480-fragmented.mp4",BYTERANGE="1376@0" | ||
7 | #EXTINF:4.000000, | ||
8 | #EXT-X-BYTERANGE:26042@1376 | ||
9 | f9377e69-d8f2-4de8-8087-ddbca6629829-480-fragmented.mp4 | ||
10 | #EXTINF:1.000000, | ||
11 | #EXT-X-BYTERANGE:12353@27418 | ||
12 | f9377e69-d8f2-4de8-8087-ddbca6629829-480-fragmented.mp4 | ||
13 | #EXT-X-ENDLIST | ||
diff --git a/packages/tests/fixtures/video_short_480p.mp4 b/packages/tests/fixtures/video_short_480p.mp4 new file mode 100644 index 000000000..e05b58b6b --- /dev/null +++ b/packages/tests/fixtures/video_short_480p.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_4k.mp4 b/packages/tests/fixtures/video_short_4k.mp4 new file mode 100644 index 000000000..402479743 --- /dev/null +++ b/packages/tests/fixtures/video_short_4k.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_720p.m3u8 b/packages/tests/fixtures/video_short_720p.m3u8 new file mode 100644 index 000000000..7cee94032 --- /dev/null +++ b/packages/tests/fixtures/video_short_720p.m3u8 | |||
@@ -0,0 +1,13 @@ | |||
1 | #EXTM3U | ||
2 | #EXT-X-VERSION:7 | ||
3 | #EXT-X-TARGETDURATION:4 | ||
4 | #EXT-X-MEDIA-SEQUENCE:0 | ||
5 | #EXT-X-PLAYLIST-TYPE:VOD | ||
6 | #EXT-X-MAP:URI="c1014aa4-d1f4-4b66-927b-c23d283fcae0-720-fragmented.mp4",BYTERANGE="1356@0" | ||
7 | #EXTINF:4.000000, | ||
8 | #EXT-X-BYTERANGE:39260@1356 | ||
9 | c1014aa4-d1f4-4b66-927b-c23d283fcae0-720-fragmented.mp4 | ||
10 | #EXTINF:1.000000, | ||
11 | #EXT-X-BYTERANGE:18493@40616 | ||
12 | c1014aa4-d1f4-4b66-927b-c23d283fcae0-720-fragmented.mp4 | ||
13 | #EXT-X-ENDLIST | ||
diff --git a/packages/tests/fixtures/video_short_720p.mp4 b/packages/tests/fixtures/video_short_720p.mp4 new file mode 100644 index 000000000..35e8f69a7 --- /dev/null +++ b/packages/tests/fixtures/video_short_720p.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_fake.webm b/packages/tests/fixtures/video_short_fake.webm new file mode 100644 index 000000000..d85290ae5 --- /dev/null +++ b/packages/tests/fixtures/video_short_fake.webm | |||
@@ -0,0 +1 @@ | |||
this is a fake video mouahahah | |||
diff --git a/packages/tests/fixtures/video_short_mp3_256k.mp4 b/packages/tests/fixtures/video_short_mp3_256k.mp4 new file mode 100644 index 000000000..4c1c7b45e --- /dev/null +++ b/packages/tests/fixtures/video_short_mp3_256k.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_short_no_audio.mp4 b/packages/tests/fixtures/video_short_no_audio.mp4 new file mode 100644 index 000000000..329d20fba --- /dev/null +++ b/packages/tests/fixtures/video_short_no_audio.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_very_long_10p.mp4 b/packages/tests/fixtures/video_very_long_10p.mp4 new file mode 100644 index 000000000..852297933 --- /dev/null +++ b/packages/tests/fixtures/video_very_long_10p.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/fixtures/video_very_short_240p.mp4 b/packages/tests/fixtures/video_very_short_240p.mp4 new file mode 100644 index 000000000..95b6be92a --- /dev/null +++ b/packages/tests/fixtures/video_very_short_240p.mp4 | |||
Binary files differ | |||
diff --git a/packages/tests/package.json b/packages/tests/package.json new file mode 100644 index 000000000..02882ebc7 --- /dev/null +++ b/packages/tests/package.json | |||
@@ -0,0 +1,12 @@ | |||
1 | { | ||
2 | "name": "@peertube/tests", | ||
3 | "private": true, | ||
4 | "version": "0.0.0", | ||
5 | "type": "module", | ||
6 | "devDependencies": {}, | ||
7 | "scripts": { | ||
8 | "build": "tsc", | ||
9 | "watch": "tsc -w" | ||
10 | }, | ||
11 | "dependencies": {} | ||
12 | } | ||
diff --git a/packages/tests/src/api/activitypub/cleaner.ts b/packages/tests/src/api/activitypub/cleaner.ts new file mode 100644 index 000000000..4476aea85 --- /dev/null +++ b/packages/tests/src/api/activitypub/cleaner.ts | |||
@@ -0,0 +1,342 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test AP cleaner', function () { | ||
16 | let servers: PeerTubeServer[] = [] | ||
17 | const sqlCommands: SQLCommand[] = [] | ||
18 | |||
19 | let videoUUID1: string | ||
20 | let videoUUID2: string | ||
21 | let videoUUID3: string | ||
22 | |||
23 | let videoUUIDs: string[] | ||
24 | |||
25 | before(async function () { | ||
26 | this.timeout(120000) | ||
27 | |||
28 | const config = { | ||
29 | federation: { | ||
30 | videos: { cleanup_remote_interactions: true } | ||
31 | } | ||
32 | } | ||
33 | servers = await createMultipleServers(3, config) | ||
34 | |||
35 | // Get the access tokens | ||
36 | await setAccessTokensToServers(servers) | ||
37 | |||
38 | await Promise.all([ | ||
39 | doubleFollow(servers[0], servers[1]), | ||
40 | doubleFollow(servers[1], servers[2]), | ||
41 | doubleFollow(servers[0], servers[2]) | ||
42 | ]) | ||
43 | |||
44 | // Update 1 local share, check 6 shares | ||
45 | |||
46 | // Create 1 comment per video | ||
47 | // Update 1 remote URL and 1 local URL on | ||
48 | |||
49 | videoUUID1 = (await servers[0].videos.quickUpload({ name: 'server 1' })).uuid | ||
50 | videoUUID2 = (await servers[1].videos.quickUpload({ name: 'server 2' })).uuid | ||
51 | videoUUID3 = (await servers[2].videos.quickUpload({ name: 'server 3' })).uuid | ||
52 | |||
53 | videoUUIDs = [ videoUUID1, videoUUID2, videoUUID3 ] | ||
54 | |||
55 | await waitJobs(servers) | ||
56 | |||
57 | for (const server of servers) { | ||
58 | for (const uuid of videoUUIDs) { | ||
59 | await server.videos.rate({ id: uuid, rating: 'like' }) | ||
60 | await server.comments.createThread({ videoId: uuid, text: 'comment' }) | ||
61 | } | ||
62 | |||
63 | sqlCommands.push(new SQLCommand(server)) | ||
64 | } | ||
65 | |||
66 | await waitJobs(servers) | ||
67 | }) | ||
68 | |||
69 | it('Should have the correct likes', async function () { | ||
70 | for (const server of servers) { | ||
71 | for (const uuid of videoUUIDs) { | ||
72 | const video = await server.videos.get({ id: uuid }) | ||
73 | |||
74 | expect(video.likes).to.equal(3) | ||
75 | expect(video.dislikes).to.equal(0) | ||
76 | } | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | it('Should destroy server 3 internal likes and correctly clean them', async function () { | ||
81 | this.timeout(20000) | ||
82 | |||
83 | await sqlCommands[2].deleteAll('accountVideoRate') | ||
84 | for (const uuid of videoUUIDs) { | ||
85 | await sqlCommands[2].setVideoField(uuid, 'likes', '0') | ||
86 | } | ||
87 | |||
88 | await wait(5000) | ||
89 | await waitJobs(servers) | ||
90 | |||
91 | // Updated rates of my video | ||
92 | { | ||
93 | const video = await servers[0].videos.get({ id: videoUUID1 }) | ||
94 | expect(video.likes).to.equal(2) | ||
95 | expect(video.dislikes).to.equal(0) | ||
96 | } | ||
97 | |||
98 | // Did not update rates of a remote video | ||
99 | { | ||
100 | const video = await servers[0].videos.get({ id: videoUUID2 }) | ||
101 | expect(video.likes).to.equal(3) | ||
102 | expect(video.dislikes).to.equal(0) | ||
103 | } | ||
104 | }) | ||
105 | |||
106 | it('Should update rates to dislikes', async function () { | ||
107 | this.timeout(20000) | ||
108 | |||
109 | for (const server of servers) { | ||
110 | for (const uuid of videoUUIDs) { | ||
111 | await server.videos.rate({ id: uuid, rating: 'dislike' }) | ||
112 | } | ||
113 | } | ||
114 | |||
115 | await waitJobs(servers) | ||
116 | |||
117 | for (const server of servers) { | ||
118 | for (const uuid of videoUUIDs) { | ||
119 | const video = await server.videos.get({ id: uuid }) | ||
120 | expect(video.likes).to.equal(0) | ||
121 | expect(video.dislikes).to.equal(3) | ||
122 | } | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | it('Should destroy server 3 internal dislikes and correctly clean them', async function () { | ||
127 | this.timeout(20000) | ||
128 | |||
129 | await sqlCommands[2].deleteAll('accountVideoRate') | ||
130 | |||
131 | for (const uuid of videoUUIDs) { | ||
132 | await sqlCommands[2].setVideoField(uuid, 'dislikes', '0') | ||
133 | } | ||
134 | |||
135 | await wait(5000) | ||
136 | await waitJobs(servers) | ||
137 | |||
138 | // Updated rates of my video | ||
139 | { | ||
140 | const video = await servers[0].videos.get({ id: videoUUID1 }) | ||
141 | expect(video.likes).to.equal(0) | ||
142 | expect(video.dislikes).to.equal(2) | ||
143 | } | ||
144 | |||
145 | // Did not update rates of a remote video | ||
146 | { | ||
147 | const video = await servers[0].videos.get({ id: videoUUID2 }) | ||
148 | expect(video.likes).to.equal(0) | ||
149 | expect(video.dislikes).to.equal(3) | ||
150 | } | ||
151 | }) | ||
152 | |||
153 | it('Should destroy server 3 internal shares and correctly clean them', async function () { | ||
154 | this.timeout(20000) | ||
155 | |||
156 | const preCount = await sqlCommands[0].getVideoShareCount() | ||
157 | expect(preCount).to.equal(6) | ||
158 | |||
159 | await sqlCommands[2].deleteAll('videoShare') | ||
160 | await wait(5000) | ||
161 | await waitJobs(servers) | ||
162 | |||
163 | // Still 6 because we don't have remote shares on local videos | ||
164 | const postCount = await sqlCommands[0].getVideoShareCount() | ||
165 | expect(postCount).to.equal(6) | ||
166 | }) | ||
167 | |||
168 | it('Should destroy server 3 internal comments and correctly clean them', async function () { | ||
169 | this.timeout(20000) | ||
170 | |||
171 | { | ||
172 | const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 }) | ||
173 | expect(total).to.equal(3) | ||
174 | } | ||
175 | |||
176 | await sqlCommands[2].deleteAll('videoComment') | ||
177 | |||
178 | await wait(5000) | ||
179 | await waitJobs(servers) | ||
180 | |||
181 | { | ||
182 | const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 }) | ||
183 | expect(total).to.equal(2) | ||
184 | } | ||
185 | }) | ||
186 | |||
187 | it('Should correctly update rate URLs', async function () { | ||
188 | this.timeout(30000) | ||
189 | |||
190 | async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') { | ||
191 | const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` + | ||
192 | `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'` | ||
193 | const res = await sqlCommands[0].selectQuery<{ url: string }>(query) | ||
194 | |||
195 | for (const rate of res) { | ||
196 | const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`) | ||
197 | expect(rate.url).to.match(matcher) | ||
198 | } | ||
199 | } | ||
200 | |||
201 | async function checkLocal () { | ||
202 | const startsWith = 'http://' + servers[0].host + '%' | ||
203 | // On local videos | ||
204 | await check(startsWith, servers[0].url, '', 'false') | ||
205 | // On remote videos | ||
206 | await check(startsWith, servers[0].url, '', 'true') | ||
207 | } | ||
208 | |||
209 | async function checkRemote (suffix: string) { | ||
210 | const startsWith = 'http://' + servers[1].host + '%' | ||
211 | // On local videos | ||
212 | await check(startsWith, servers[1].url, suffix, 'false') | ||
213 | // On remote videos, we should not update URLs so no suffix | ||
214 | await check(startsWith, servers[1].url, '', 'true') | ||
215 | } | ||
216 | |||
217 | await checkLocal() | ||
218 | await checkRemote('') | ||
219 | |||
220 | { | ||
221 | const query = `UPDATE "accountVideoRate" SET url = url || 'stan'` | ||
222 | await sqlCommands[1].updateQuery(query) | ||
223 | |||
224 | await wait(5000) | ||
225 | await waitJobs(servers) | ||
226 | } | ||
227 | |||
228 | await checkLocal() | ||
229 | await checkRemote('stan') | ||
230 | }) | ||
231 | |||
232 | it('Should correctly update comment URLs', async function () { | ||
233 | this.timeout(30000) | ||
234 | |||
235 | async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') { | ||
236 | const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` + | ||
237 | `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'` | ||
238 | |||
239 | const res = await sqlCommands[0].selectQuery<{ url: string, videoUUID: string }>(query) | ||
240 | |||
241 | for (const comment of res) { | ||
242 | const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`) | ||
243 | expect(comment.url).to.match(matcher) | ||
244 | } | ||
245 | } | ||
246 | |||
247 | async function checkLocal () { | ||
248 | const startsWith = 'http://' + servers[0].host + '%' | ||
249 | // On local videos | ||
250 | await check(startsWith, servers[0].url, '', 'false') | ||
251 | // On remote videos | ||
252 | await check(startsWith, servers[0].url, '', 'true') | ||
253 | } | ||
254 | |||
255 | async function checkRemote (suffix: string) { | ||
256 | const startsWith = 'http://' + servers[1].host + '%' | ||
257 | // On local videos | ||
258 | await check(startsWith, servers[1].url, suffix, 'false') | ||
259 | // On remote videos, we should not update URLs so no suffix | ||
260 | await check(startsWith, servers[1].url, '', 'true') | ||
261 | } | ||
262 | |||
263 | { | ||
264 | const query = `UPDATE "videoComment" SET url = url || 'kyle'` | ||
265 | await sqlCommands[1].updateQuery(query) | ||
266 | |||
267 | await wait(5000) | ||
268 | await waitJobs(servers) | ||
269 | } | ||
270 | |||
271 | await checkLocal() | ||
272 | await checkRemote('kyle') | ||
273 | }) | ||
274 | |||
275 | it('Should remove unavailable remote resources', async function () { | ||
276 | this.timeout(240000) | ||
277 | |||
278 | async function expectNotDeleted () { | ||
279 | { | ||
280 | const video = await servers[0].videos.get({ id: uuid }) | ||
281 | |||
282 | expect(video.likes).to.equal(3) | ||
283 | expect(video.dislikes).to.equal(0) | ||
284 | } | ||
285 | |||
286 | { | ||
287 | const { total } = await servers[0].comments.listThreads({ videoId: uuid }) | ||
288 | expect(total).to.equal(3) | ||
289 | } | ||
290 | } | ||
291 | |||
292 | async function expectDeleted () { | ||
293 | { | ||
294 | const video = await servers[0].videos.get({ id: uuid }) | ||
295 | |||
296 | expect(video.likes).to.equal(2) | ||
297 | expect(video.dislikes).to.equal(0) | ||
298 | } | ||
299 | |||
300 | { | ||
301 | const { total } = await servers[0].comments.listThreads({ videoId: uuid }) | ||
302 | expect(total).to.equal(2) | ||
303 | } | ||
304 | } | ||
305 | |||
306 | const uuid = (await servers[0].videos.quickUpload({ name: 'server 1 video 2' })).uuid | ||
307 | |||
308 | await waitJobs(servers) | ||
309 | |||
310 | for (const server of servers) { | ||
311 | await server.videos.rate({ id: uuid, rating: 'like' }) | ||
312 | await server.comments.createThread({ videoId: uuid, text: 'comment' }) | ||
313 | } | ||
314 | |||
315 | await waitJobs(servers) | ||
316 | |||
317 | await expectNotDeleted() | ||
318 | |||
319 | await servers[1].kill() | ||
320 | |||
321 | await wait(5000) | ||
322 | await expectNotDeleted() | ||
323 | |||
324 | let continueWhile = true | ||
325 | |||
326 | do { | ||
327 | try { | ||
328 | await expectDeleted() | ||
329 | continueWhile = false | ||
330 | } catch { | ||
331 | } | ||
332 | } while (continueWhile) | ||
333 | }) | ||
334 | |||
335 | after(async function () { | ||
336 | for (const sql of sqlCommands) { | ||
337 | await sql.cleanup() | ||
338 | } | ||
339 | |||
340 | await cleanupTests(servers) | ||
341 | }) | ||
342 | }) | ||
diff --git a/packages/tests/src/api/activitypub/client.ts b/packages/tests/src/api/activitypub/client.ts new file mode 100644 index 000000000..fb9575d31 --- /dev/null +++ b/packages/tests/src/api/activitypub/client.ts | |||
@@ -0,0 +1,136 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { processViewersStats } from '@tests/shared/views.js' | ||
5 | import { HttpStatusCode, VideoPlaylistPrivacy, WatchActionObject } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | makeActivityPubGetRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Test activitypub', function () { | ||
17 | let servers: PeerTubeServer[] = [] | ||
18 | let video: { id: number, uuid: string, shortUUID: string } | ||
19 | let playlist: { id: number, uuid: string, shortUUID: string } | ||
20 | |||
21 | async function testAccount (path: string) { | ||
22 | const res = await makeActivityPubGetRequest(servers[0].url, path) | ||
23 | const object = res.body | ||
24 | |||
25 | expect(object.type).to.equal('Person') | ||
26 | expect(object.id).to.equal(servers[0].url + '/accounts/root') | ||
27 | expect(object.name).to.equal('root') | ||
28 | expect(object.preferredUsername).to.equal('root') | ||
29 | } | ||
30 | |||
31 | async function testChannel (path: string) { | ||
32 | const res = await makeActivityPubGetRequest(servers[0].url, path) | ||
33 | const object = res.body | ||
34 | |||
35 | expect(object.type).to.equal('Group') | ||
36 | expect(object.id).to.equal(servers[0].url + '/video-channels/root_channel') | ||
37 | expect(object.name).to.equal('Main root channel') | ||
38 | expect(object.preferredUsername).to.equal('root_channel') | ||
39 | } | ||
40 | |||
41 | async function testVideo (path: string) { | ||
42 | const res = await makeActivityPubGetRequest(servers[0].url, path) | ||
43 | const object = res.body | ||
44 | |||
45 | expect(object.type).to.equal('Video') | ||
46 | expect(object.id).to.equal(servers[0].url + '/videos/watch/' + video.uuid) | ||
47 | expect(object.name).to.equal('video') | ||
48 | } | ||
49 | |||
50 | async function testPlaylist (path: string) { | ||
51 | const res = await makeActivityPubGetRequest(servers[0].url, path) | ||
52 | const object = res.body | ||
53 | |||
54 | expect(object.type).to.equal('Playlist') | ||
55 | expect(object.id).to.equal(servers[0].url + '/video-playlists/' + playlist.uuid) | ||
56 | expect(object.name).to.equal('playlist') | ||
57 | } | ||
58 | |||
59 | before(async function () { | ||
60 | this.timeout(30000) | ||
61 | |||
62 | servers = await createMultipleServers(2) | ||
63 | |||
64 | await setAccessTokensToServers(servers) | ||
65 | await setDefaultVideoChannel(servers) | ||
66 | |||
67 | { | ||
68 | video = await servers[0].videos.quickUpload({ name: 'video' }) | ||
69 | } | ||
70 | |||
71 | { | ||
72 | const attributes = { displayName: 'playlist', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[0].store.channel.id } | ||
73 | playlist = await servers[0].playlists.create({ attributes }) | ||
74 | } | ||
75 | |||
76 | await doubleFollow(servers[0], servers[1]) | ||
77 | }) | ||
78 | |||
79 | it('Should return the account object', async function () { | ||
80 | await testAccount('/accounts/root') | ||
81 | await testAccount('/a/root') | ||
82 | }) | ||
83 | |||
84 | it('Should return the channel object', async function () { | ||
85 | await testChannel('/video-channels/root_channel') | ||
86 | await testChannel('/c/root_channel') | ||
87 | }) | ||
88 | |||
89 | it('Should return the video object', async function () { | ||
90 | await testVideo('/videos/watch/' + video.id) | ||
91 | await testVideo('/videos/watch/' + video.uuid) | ||
92 | await testVideo('/videos/watch/' + video.shortUUID) | ||
93 | await testVideo('/w/' + video.id) | ||
94 | await testVideo('/w/' + video.uuid) | ||
95 | await testVideo('/w/' + video.shortUUID) | ||
96 | }) | ||
97 | |||
98 | it('Should return the playlist object', async function () { | ||
99 | await testPlaylist('/video-playlists/' + playlist.id) | ||
100 | await testPlaylist('/video-playlists/' + playlist.uuid) | ||
101 | await testPlaylist('/video-playlists/' + playlist.shortUUID) | ||
102 | await testPlaylist('/w/p/' + playlist.id) | ||
103 | await testPlaylist('/w/p/' + playlist.uuid) | ||
104 | await testPlaylist('/w/p/' + playlist.shortUUID) | ||
105 | await testPlaylist('/videos/watch/playlist/' + playlist.id) | ||
106 | await testPlaylist('/videos/watch/playlist/' + playlist.uuid) | ||
107 | await testPlaylist('/videos/watch/playlist/' + playlist.shortUUID) | ||
108 | }) | ||
109 | |||
110 | it('Should redirect to the origin video object', async function () { | ||
111 | const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + video.uuid, HttpStatusCode.FOUND_302) | ||
112 | |||
113 | expect(res.header.location).to.equal(servers[0].url + '/videos/watch/' + video.uuid) | ||
114 | }) | ||
115 | |||
116 | it('Should return the watch action', async function () { | ||
117 | this.timeout(50000) | ||
118 | |||
119 | await servers[0].views.simulateViewer({ id: video.uuid, currentTimes: [ 0, 2 ] }) | ||
120 | await processViewersStats(servers) | ||
121 | |||
122 | const res = await makeActivityPubGetRequest(servers[0].url, '/videos/local-viewer/1', HttpStatusCode.OK_200) | ||
123 | |||
124 | const object: WatchActionObject = res.body | ||
125 | expect(object.type).to.equal('WatchAction') | ||
126 | expect(object.duration).to.equal('PT2S') | ||
127 | expect(object.actionStatus).to.equal('CompletedActionStatus') | ||
128 | expect(object.watchSections).to.have.lengthOf(1) | ||
129 | expect(object.watchSections[0].startTimestamp).to.equal(0) | ||
130 | expect(object.watchSections[0].endTimestamp).to.equal(2) | ||
131 | }) | ||
132 | |||
133 | after(async function () { | ||
134 | await cleanupTests(servers) | ||
135 | }) | ||
136 | }) | ||
diff --git a/packages/tests/src/api/activitypub/fetch.ts b/packages/tests/src/api/activitypub/fetch.ts new file mode 100644 index 000000000..c7f5288cc --- /dev/null +++ b/packages/tests/src/api/activitypub/fetch.ts | |||
@@ -0,0 +1,82 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test ActivityPub fetcher', function () { | ||
15 | let servers: PeerTubeServer[] | ||
16 | let sqlCommandServer1: SQLCommand | ||
17 | |||
18 | // --------------------------------------------------------------- | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(60000) | ||
22 | |||
23 | servers = await createMultipleServers(3) | ||
24 | |||
25 | // Get the access tokens | ||
26 | await setAccessTokensToServers(servers) | ||
27 | |||
28 | const user = { username: 'user1', password: 'password' } | ||
29 | for (const server of servers) { | ||
30 | await server.users.create({ username: user.username, password: user.password }) | ||
31 | } | ||
32 | |||
33 | const userAccessToken = await servers[0].login.getAccessToken(user) | ||
34 | |||
35 | await servers[0].videos.upload({ attributes: { name: 'video root' } }) | ||
36 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'bad video root' } }) | ||
37 | await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'video user' } }) | ||
38 | |||
39 | sqlCommandServer1 = new SQLCommand(servers[0]) | ||
40 | |||
41 | { | ||
42 | const to = servers[0].url + '/accounts/user1' | ||
43 | const value = servers[1].url + '/accounts/user1' | ||
44 | await sqlCommandServer1.setActorField(to, 'url', value) | ||
45 | } | ||
46 | |||
47 | { | ||
48 | const value = servers[2].url + '/videos/watch/' + uuid | ||
49 | await sqlCommandServer1.setVideoField(uuid, 'url', value) | ||
50 | } | ||
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 { total, data } = await servers[0].videos.list({ sort: 'createdAt' }) | ||
61 | |||
62 | expect(total).to.equal(3) | ||
63 | expect(data[0].name).to.equal('video root') | ||
64 | expect(data[1].name).to.equal('bad video root') | ||
65 | expect(data[2].name).to.equal('video user') | ||
66 | } | ||
67 | |||
68 | { | ||
69 | const { total, data } = await servers[1].videos.list({ sort: 'createdAt' }) | ||
70 | |||
71 | expect(total).to.equal(1) | ||
72 | expect(data[0].name).to.equal('video root') | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | after(async function () { | ||
77 | this.timeout(20000) | ||
78 | |||
79 | await sqlCommandServer1.cleanup() | ||
80 | await cleanupTests(servers) | ||
81 | }) | ||
82 | }) | ||
diff --git a/packages/tests/src/api/activitypub/index.ts b/packages/tests/src/api/activitypub/index.ts new file mode 100644 index 000000000..ef4f1aafb --- /dev/null +++ b/packages/tests/src/api/activitypub/index.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | import './cleaner.js' | ||
2 | import './client.js' | ||
3 | import './fetch.js' | ||
4 | import './refresher.js' | ||
5 | import './security.js' | ||
diff --git a/packages/tests/src/api/activitypub/refresher.ts b/packages/tests/src/api/activitypub/refresher.ts new file mode 100644 index 000000000..90aa1a5ad --- /dev/null +++ b/packages/tests/src/api/activitypub/refresher.ts | |||
@@ -0,0 +1,157 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test AP refresher', function () { | ||
18 | let servers: PeerTubeServer[] = [] | ||
19 | let sqlCommandServer2: SQLCommand | ||
20 | let videoUUID1: string | ||
21 | let videoUUID2: string | ||
22 | let videoUUID3: string | ||
23 | let playlistUUID1: string | ||
24 | let playlistUUID2: string | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(240000) | ||
28 | |||
29 | servers = await createMultipleServers(2) | ||
30 | |||
31 | // Get the access tokens | ||
32 | await setAccessTokensToServers(servers) | ||
33 | await setDefaultVideoChannel(servers) | ||
34 | |||
35 | for (const server of servers) { | ||
36 | await server.config.disableTranscoding() | ||
37 | } | ||
38 | |||
39 | { | ||
40 | videoUUID1 = (await servers[1].videos.quickUpload({ name: 'video1' })).uuid | ||
41 | videoUUID2 = (await servers[1].videos.quickUpload({ name: 'video2' })).uuid | ||
42 | videoUUID3 = (await servers[1].videos.quickUpload({ name: 'video3' })).uuid | ||
43 | } | ||
44 | |||
45 | { | ||
46 | const token1 = await servers[1].users.generateUserAndToken('user1') | ||
47 | await servers[1].videos.upload({ token: token1, attributes: { name: 'video4' } }) | ||
48 | |||
49 | const token2 = await servers[1].users.generateUserAndToken('user2') | ||
50 | await servers[1].videos.upload({ token: token2, attributes: { name: 'video5' } }) | ||
51 | } | ||
52 | |||
53 | { | ||
54 | const attributes = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id } | ||
55 | const created = await servers[1].playlists.create({ attributes }) | ||
56 | playlistUUID1 = created.uuid | ||
57 | } | ||
58 | |||
59 | { | ||
60 | const attributes = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id } | ||
61 | const created = await servers[1].playlists.create({ attributes }) | ||
62 | playlistUUID2 = created.uuid | ||
63 | } | ||
64 | |||
65 | await doubleFollow(servers[0], servers[1]) | ||
66 | |||
67 | sqlCommandServer2 = new SQLCommand(servers[1]) | ||
68 | }) | ||
69 | |||
70 | describe('Videos refresher', function () { | ||
71 | |||
72 | it('Should remove a deleted remote video', async function () { | ||
73 | this.timeout(60000) | ||
74 | |||
75 | await wait(10000) | ||
76 | |||
77 | // Change UUID so the remote server returns a 404 | ||
78 | await sqlCommandServer2.setVideoField(videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f') | ||
79 | |||
80 | await servers[0].videos.get({ id: videoUUID1 }) | ||
81 | await servers[0].videos.get({ id: videoUUID2 }) | ||
82 | |||
83 | await waitJobs(servers) | ||
84 | |||
85 | await servers[0].videos.get({ id: videoUUID1, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
86 | await servers[0].videos.get({ id: videoUUID2 }) | ||
87 | }) | ||
88 | |||
89 | it('Should not update a remote video if the remote instance is down', async function () { | ||
90 | this.timeout(70000) | ||
91 | |||
92 | await killallServers([ servers[1] ]) | ||
93 | |||
94 | await sqlCommandServer2.setVideoField(videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e') | ||
95 | |||
96 | // Video will need a refresh | ||
97 | await wait(10000) | ||
98 | |||
99 | await servers[0].videos.get({ id: videoUUID3 }) | ||
100 | // The refresh should fail | ||
101 | await waitJobs([ servers[0] ]) | ||
102 | |||
103 | await servers[1].run() | ||
104 | |||
105 | await servers[0].videos.get({ id: videoUUID3 }) | ||
106 | }) | ||
107 | }) | ||
108 | |||
109 | describe('Actors refresher', function () { | ||
110 | |||
111 | it('Should remove a deleted actor', async function () { | ||
112 | this.timeout(60000) | ||
113 | |||
114 | const command = servers[0].accounts | ||
115 | |||
116 | await wait(10000) | ||
117 | |||
118 | // Change actor name so the remote server returns a 404 | ||
119 | const to = servers[1].url + '/accounts/user2' | ||
120 | await sqlCommandServer2.setActorField(to, 'preferredUsername', 'toto') | ||
121 | |||
122 | await command.get({ accountName: 'user1@' + servers[1].host }) | ||
123 | await command.get({ accountName: 'user2@' + servers[1].host }) | ||
124 | |||
125 | await waitJobs(servers) | ||
126 | |||
127 | await command.get({ accountName: 'user1@' + servers[1].host, expectedStatus: HttpStatusCode.OK_200 }) | ||
128 | await command.get({ accountName: 'user2@' + servers[1].host, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
129 | }) | ||
130 | }) | ||
131 | |||
132 | describe('Playlist refresher', function () { | ||
133 | |||
134 | it('Should remove a deleted playlist', async function () { | ||
135 | this.timeout(60000) | ||
136 | |||
137 | await wait(10000) | ||
138 | |||
139 | // Change UUID so the remote server returns a 404 | ||
140 | await sqlCommandServer2.setPlaylistField(playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e') | ||
141 | |||
142 | await servers[0].playlists.get({ playlistId: playlistUUID1 }) | ||
143 | await servers[0].playlists.get({ playlistId: playlistUUID2 }) | ||
144 | |||
145 | await waitJobs(servers) | ||
146 | |||
147 | await servers[0].playlists.get({ playlistId: playlistUUID1, expectedStatus: HttpStatusCode.OK_200 }) | ||
148 | await servers[0].playlists.get({ playlistId: playlistUUID2, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
149 | }) | ||
150 | }) | ||
151 | |||
152 | after(async function () { | ||
153 | await sqlCommandServer2.cleanup() | ||
154 | |||
155 | await cleanupTests(servers) | ||
156 | }) | ||
157 | }) | ||
diff --git a/packages/tests/src/api/activitypub/security.ts b/packages/tests/src/api/activitypub/security.ts new file mode 100644 index 000000000..d9649de50 --- /dev/null +++ b/packages/tests/src/api/activitypub/security.ts | |||
@@ -0,0 +1,331 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
6 | import { PeerTubeServer, cleanupTests, createMultipleServers, killallServers } from '@peertube/peertube-server-commands' | ||
7 | import { | ||
8 | activityPubContextify, | ||
9 | buildGlobalHTTPHeaders, | ||
10 | signAndContextify | ||
11 | } from '@peertube/peertube-server/server/helpers/activity-pub-utils.js' | ||
12 | import { buildDigest } from '@peertube/peertube-server/server/helpers/peertube-crypto.js' | ||
13 | import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@peertube/peertube-server/server/initializers/constants.js' | ||
14 | import { makePOSTAPRequest } from '@tests/shared/requests.js' | ||
15 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
16 | import { expect } from 'chai' | ||
17 | import { readJsonSync } from 'fs-extra/esm' | ||
18 | |||
19 | function fakeFilter () { | ||
20 | return (data: any) => Promise.resolve(data) | ||
21 | } | ||
22 | |||
23 | function setKeysOfServer (onServer: SQLCommand, ofServerUrl: string, publicKey: string, privateKey: string) { | ||
24 | const url = ofServerUrl + '/accounts/peertube' | ||
25 | |||
26 | return Promise.all([ | ||
27 | onServer.setActorField(url, 'publicKey', publicKey), | ||
28 | onServer.setActorField(url, 'privateKey', privateKey) | ||
29 | ]) | ||
30 | } | ||
31 | |||
32 | function setUpdatedAtOfServer (onServer: SQLCommand, ofServerUrl: string, updatedAt: string) { | ||
33 | const url = ofServerUrl + '/accounts/peertube' | ||
34 | |||
35 | return Promise.all([ | ||
36 | onServer.setActorField(url, 'createdAt', updatedAt), | ||
37 | onServer.setActorField(url, 'updatedAt', updatedAt) | ||
38 | ]) | ||
39 | } | ||
40 | |||
41 | function getAnnounceWithoutContext (server: PeerTubeServer) { | ||
42 | const json = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) | ||
43 | const result: typeof json = {} | ||
44 | |||
45 | for (const key of Object.keys(json)) { | ||
46 | if (Array.isArray(json[key])) { | ||
47 | result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`)) | ||
48 | } else { | ||
49 | result[key] = json[key].replace(':9002', `:${server.port}`) | ||
50 | } | ||
51 | } | ||
52 | |||
53 | return result | ||
54 | } | ||
55 | |||
56 | async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { | ||
57 | const follow = { | ||
58 | type: 'Follow', | ||
59 | id: by.url + '/' + new Date().getTime(), | ||
60 | actor: by.url, | ||
61 | object: to.url | ||
62 | } | ||
63 | |||
64 | const body = await activityPubContextify(follow, 'Follow', fakeFilter()) | ||
65 | |||
66 | const httpSignature = { | ||
67 | algorithm: HTTP_SIGNATURE.ALGORITHM, | ||
68 | authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, | ||
69 | keyId: by.url, | ||
70 | key: by.privateKey, | ||
71 | headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD | ||
72 | } | ||
73 | const headers = { | ||
74 | 'digest': buildDigest(body), | ||
75 | 'content-type': 'application/activity+json', | ||
76 | 'accept': ACTIVITY_PUB.ACCEPT_HEADER | ||
77 | } | ||
78 | |||
79 | return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers) | ||
80 | } | ||
81 | |||
82 | describe('Test ActivityPub security', function () { | ||
83 | let servers: PeerTubeServer[] | ||
84 | let sqlCommands: SQLCommand[] = [] | ||
85 | |||
86 | let url: string | ||
87 | |||
88 | const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/keys.json')) | ||
89 | const invalidKeys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json')) | ||
90 | const baseHttpSignature = () => ({ | ||
91 | algorithm: HTTP_SIGNATURE.ALGORITHM, | ||
92 | authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, | ||
93 | keyId: 'acct:peertube@' + servers[1].host, | ||
94 | key: keys.privateKey, | ||
95 | headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD | ||
96 | }) | ||
97 | |||
98 | // --------------------------------------------------------------- | ||
99 | |||
100 | before(async function () { | ||
101 | this.timeout(60000) | ||
102 | |||
103 | servers = await createMultipleServers(3) | ||
104 | |||
105 | sqlCommands = servers.map(s => new SQLCommand(s)) | ||
106 | |||
107 | url = servers[0].url + '/inbox' | ||
108 | |||
109 | await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, null) | ||
110 | await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey) | ||
111 | |||
112 | const to = { url: servers[0].url + '/accounts/peertube' } | ||
113 | const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey } | ||
114 | await makeFollowRequest(to, by) | ||
115 | }) | ||
116 | |||
117 | describe('When checking HTTP signature', function () { | ||
118 | |||
119 | it('Should fail with an invalid digest', async function () { | ||
120 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter()) | ||
121 | const headers = { | ||
122 | Digest: buildDigest({ hello: 'coucou' }) | ||
123 | } | ||
124 | |||
125 | try { | ||
126 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | ||
127 | expect(true, 'Did not throw').to.be.false | ||
128 | } catch (err) { | ||
129 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
130 | } | ||
131 | }) | ||
132 | |||
133 | it('Should fail with an invalid date', async function () { | ||
134 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter()) | ||
135 | const headers = buildGlobalHTTPHeaders(body) | ||
136 | headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' | ||
137 | |||
138 | try { | ||
139 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | ||
140 | expect(true, 'Did not throw').to.be.false | ||
141 | } catch (err) { | ||
142 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
143 | } | ||
144 | }) | ||
145 | |||
146 | it('Should fail with bad keys', async function () { | ||
147 | await setKeysOfServer(sqlCommands[0], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
148 | await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
149 | |||
150 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter()) | ||
151 | const headers = buildGlobalHTTPHeaders(body) | ||
152 | |||
153 | try { | ||
154 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | ||
155 | expect(true, 'Did not throw').to.be.false | ||
156 | } catch (err) { | ||
157 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
158 | } | ||
159 | }) | ||
160 | |||
161 | it('Should reject requests without appropriate signed headers', async function () { | ||
162 | await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey) | ||
163 | await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey) | ||
164 | |||
165 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter()) | ||
166 | const headers = buildGlobalHTTPHeaders(body) | ||
167 | |||
168 | const signatureOptions = baseHttpSignature() | ||
169 | const badHeadersMatrix = [ | ||
170 | [ '(request-target)', 'date', 'digest' ], | ||
171 | [ 'host', 'date', 'digest' ], | ||
172 | [ '(request-target)', 'host', 'digest' ] | ||
173 | ] | ||
174 | |||
175 | for (const badHeaders of badHeadersMatrix) { | ||
176 | signatureOptions.headers = badHeaders | ||
177 | |||
178 | try { | ||
179 | await makePOSTAPRequest(url, body, signatureOptions, headers) | ||
180 | expect(true, 'Did not throw').to.be.false | ||
181 | } catch (err) { | ||
182 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
183 | } | ||
184 | } | ||
185 | }) | ||
186 | |||
187 | it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () { | ||
188 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter()) | ||
189 | const headers = buildGlobalHTTPHeaders(body) | ||
190 | |||
191 | const signatureOptions = baseHttpSignature() | ||
192 | signatureOptions.headers = [ '(request-target)', '(created)', 'host', 'digest' ] | ||
193 | |||
194 | const { statusCode } = await makePOSTAPRequest(url, body, signatureOptions, headers) | ||
195 | expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | ||
196 | }) | ||
197 | |||
198 | it('Should succeed with a valid HTTP signature', async function () { | ||
199 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter()) | ||
200 | const headers = buildGlobalHTTPHeaders(body) | ||
201 | |||
202 | const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | ||
203 | expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | ||
204 | }) | ||
205 | |||
206 | it('Should refresh the actor keys', async function () { | ||
207 | this.timeout(20000) | ||
208 | |||
209 | // Update keys of server 2 to invalid keys | ||
210 | // Server 1 should refresh the actor and fail | ||
211 | await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
212 | await setUpdatedAtOfServer(sqlCommands[0], servers[1].url, '2015-07-17 22:00:00+00') | ||
213 | |||
214 | // Invalid peertube actor cache | ||
215 | await killallServers([ servers[1] ]) | ||
216 | await servers[1].run() | ||
217 | |||
218 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter()) | ||
219 | const headers = buildGlobalHTTPHeaders(body) | ||
220 | |||
221 | try { | ||
222 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | ||
223 | expect(true, 'Did not throw').to.be.false | ||
224 | } catch (err) { | ||
225 | console.error(err) | ||
226 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
227 | } | ||
228 | }) | ||
229 | }) | ||
230 | |||
231 | describe('When checking Linked Data Signature', function () { | ||
232 | before(async function () { | ||
233 | await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey) | ||
234 | await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey) | ||
235 | await setKeysOfServer(sqlCommands[2], servers[2].url, keys.publicKey, keys.privateKey) | ||
236 | |||
237 | const to = { url: servers[0].url + '/accounts/peertube' } | ||
238 | const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey } | ||
239 | await makeFollowRequest(to, by) | ||
240 | }) | ||
241 | |||
242 | it('Should fail with bad keys', async function () { | ||
243 | await setKeysOfServer(sqlCommands[0], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
244 | await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
245 | |||
246 | const body = getAnnounceWithoutContext(servers[1]) | ||
247 | body.actor = servers[2].url + '/accounts/peertube' | ||
248 | |||
249 | const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' } | ||
250 | const signedBody = await signAndContextify(signer, body, 'Announce', fakeFilter()) | ||
251 | |||
252 | const headers = buildGlobalHTTPHeaders(signedBody) | ||
253 | |||
254 | try { | ||
255 | await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | ||
256 | expect(true, 'Did not throw').to.be.false | ||
257 | } catch (err) { | ||
258 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
259 | } | ||
260 | }) | ||
261 | |||
262 | it('Should fail with an altered body', async function () { | ||
263 | await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey) | ||
264 | await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey) | ||
265 | |||
266 | const body = getAnnounceWithoutContext(servers[1]) | ||
267 | body.actor = servers[2].url + '/accounts/peertube' | ||
268 | |||
269 | const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' } | ||
270 | const signedBody = await signAndContextify(signer, body, 'Announce', fakeFilter()) | ||
271 | |||
272 | signedBody.actor = servers[2].url + '/account/peertube' | ||
273 | |||
274 | const headers = buildGlobalHTTPHeaders(signedBody) | ||
275 | |||
276 | try { | ||
277 | await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | ||
278 | expect(true, 'Did not throw').to.be.false | ||
279 | } catch (err) { | ||
280 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
281 | } | ||
282 | }) | ||
283 | |||
284 | it('Should succeed with a valid signature', async function () { | ||
285 | const body = getAnnounceWithoutContext(servers[1]) | ||
286 | body.actor = servers[2].url + '/accounts/peertube' | ||
287 | |||
288 | const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' } | ||
289 | const signedBody = await signAndContextify(signer, body, 'Announce', fakeFilter()) | ||
290 | |||
291 | const headers = buildGlobalHTTPHeaders(signedBody) | ||
292 | |||
293 | const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | ||
294 | expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | ||
295 | }) | ||
296 | |||
297 | it('Should refresh the actor keys', async function () { | ||
298 | this.timeout(20000) | ||
299 | |||
300 | // Wait refresh invalidation | ||
301 | await wait(10000) | ||
302 | |||
303 | // Update keys of server 3 to invalid keys | ||
304 | // Server 1 should refresh the actor and fail | ||
305 | await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
306 | |||
307 | const body = getAnnounceWithoutContext(servers[1]) | ||
308 | body.actor = servers[2].url + '/accounts/peertube' | ||
309 | |||
310 | const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' } | ||
311 | const signedBody = await signAndContextify(signer, body, 'Announce', fakeFilter()) | ||
312 | |||
313 | const headers = buildGlobalHTTPHeaders(signedBody) | ||
314 | |||
315 | try { | ||
316 | await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | ||
317 | expect(true, 'Did not throw').to.be.false | ||
318 | } catch (err) { | ||
319 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
320 | } | ||
321 | }) | ||
322 | }) | ||
323 | |||
324 | after(async function () { | ||
325 | for (const sql of sqlCommands) { | ||
326 | await sql.cleanup() | ||
327 | } | ||
328 | |||
329 | await cleanupTests(servers) | ||
330 | }) | ||
331 | }) | ||
diff --git a/packages/tests/src/api/check-params/abuses.ts b/packages/tests/src/api/check-params/abuses.ts new file mode 100644 index 000000000..1effc82b1 --- /dev/null +++ b/packages/tests/src/api/check-params/abuses.ts | |||
@@ -0,0 +1,438 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { AbuseCreate, AbuseState, HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | AbusesCommand, | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | doubleFollow, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test abuses API validators', function () { | ||
18 | const basePath = '/api/v1/abuses/' | ||
19 | |||
20 | let server: PeerTubeServer | ||
21 | |||
22 | let userToken = '' | ||
23 | let userToken2 = '' | ||
24 | let abuseId: number | ||
25 | let messageId: number | ||
26 | |||
27 | let command: AbusesCommand | ||
28 | |||
29 | // --------------------------------------------------------------- | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(30000) | ||
33 | |||
34 | server = await createSingleServer(1) | ||
35 | |||
36 | await setAccessTokensToServers([ server ]) | ||
37 | |||
38 | userToken = await server.users.generateUserAndToken('user_1') | ||
39 | userToken2 = await server.users.generateUserAndToken('user_2') | ||
40 | |||
41 | server.store.videoCreated = await server.videos.upload() | ||
42 | |||
43 | command = server.abuses | ||
44 | }) | ||
45 | |||
46 | describe('When listing abuses for admins', function () { | ||
47 | const path = basePath | ||
48 | |||
49 | it('Should fail with a bad start pagination', async function () { | ||
50 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
51 | }) | ||
52 | |||
53 | it('Should fail with a bad count pagination', async function () { | ||
54 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
55 | }) | ||
56 | |||
57 | it('Should fail with an incorrect sort', async function () { | ||
58 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
59 | }) | ||
60 | |||
61 | it('Should fail with a non authenticated user', async function () { | ||
62 | await makeGetRequest({ | ||
63 | url: server.url, | ||
64 | path, | ||
65 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
66 | }) | ||
67 | }) | ||
68 | |||
69 | it('Should fail with a non admin user', async function () { | ||
70 | await makeGetRequest({ | ||
71 | url: server.url, | ||
72 | path, | ||
73 | token: userToken, | ||
74 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
75 | }) | ||
76 | }) | ||
77 | |||
78 | it('Should fail with a bad id filter', async function () { | ||
79 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } }) | ||
80 | }) | ||
81 | |||
82 | it('Should fail with a bad filter', async function () { | ||
83 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } }) | ||
84 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } }) | ||
85 | }) | ||
86 | |||
87 | it('Should fail with bad predefined reason', async function () { | ||
88 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } }) | ||
89 | }) | ||
90 | |||
91 | it('Should fail with a bad state filter', async function () { | ||
92 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } }) | ||
93 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } }) | ||
94 | }) | ||
95 | |||
96 | it('Should fail with a bad videoIs filter', async function () { | ||
97 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } }) | ||
98 | }) | ||
99 | |||
100 | it('Should succeed with the correct params', async function () { | ||
101 | const query = { | ||
102 | id: 13, | ||
103 | predefinedReason: 'violentOrRepulsive', | ||
104 | filter: 'comment', | ||
105 | state: 2, | ||
106 | videoIs: 'deleted' | ||
107 | } | ||
108 | |||
109 | await makeGetRequest({ url: server.url, path, token: server.accessToken, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
110 | }) | ||
111 | }) | ||
112 | |||
113 | describe('When listing abuses for users', function () { | ||
114 | const path = '/api/v1/users/me/abuses' | ||
115 | |||
116 | it('Should fail with a bad start pagination', async function () { | ||
117 | await checkBadStartPagination(server.url, path, userToken) | ||
118 | }) | ||
119 | |||
120 | it('Should fail with a bad count pagination', async function () { | ||
121 | await checkBadCountPagination(server.url, path, userToken) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with an incorrect sort', async function () { | ||
125 | await checkBadSortPagination(server.url, path, userToken) | ||
126 | }) | ||
127 | |||
128 | it('Should fail with a non authenticated user', async function () { | ||
129 | await makeGetRequest({ | ||
130 | url: server.url, | ||
131 | path, | ||
132 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
133 | }) | ||
134 | }) | ||
135 | |||
136 | it('Should fail with a bad id filter', async function () { | ||
137 | await makeGetRequest({ url: server.url, path, token: userToken, query: { id: 'toto' } }) | ||
138 | }) | ||
139 | |||
140 | it('Should fail with a bad state filter', async function () { | ||
141 | await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 'toto' } }) | ||
142 | await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 0 } }) | ||
143 | }) | ||
144 | |||
145 | it('Should succeed with the correct params', async function () { | ||
146 | const query = { | ||
147 | id: 13, | ||
148 | state: 2 | ||
149 | } | ||
150 | |||
151 | await makeGetRequest({ url: server.url, path, token: userToken, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
152 | }) | ||
153 | }) | ||
154 | |||
155 | describe('When reporting an abuse', function () { | ||
156 | const path = basePath | ||
157 | |||
158 | it('Should fail with nothing', async function () { | ||
159 | const fields = {} | ||
160 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
161 | }) | ||
162 | |||
163 | it('Should fail with a wrong video', async function () { | ||
164 | const fields = { video: { id: 'blabla' }, reason: 'my super reason' } | ||
165 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
166 | }) | ||
167 | |||
168 | it('Should fail with an unknown video', async function () { | ||
169 | const fields = { video: { id: 42 }, reason: 'my super reason' } | ||
170 | await makePostBodyRequest({ | ||
171 | url: server.url, | ||
172 | path, | ||
173 | token: userToken, | ||
174 | fields, | ||
175 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
176 | }) | ||
177 | }) | ||
178 | |||
179 | it('Should fail with a wrong comment', async function () { | ||
180 | const fields = { comment: { id: 'blabla' }, reason: 'my super reason' } | ||
181 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
182 | }) | ||
183 | |||
184 | it('Should fail with an unknown comment', async function () { | ||
185 | const fields = { comment: { id: 42 }, reason: 'my super reason' } | ||
186 | await makePostBodyRequest({ | ||
187 | url: server.url, | ||
188 | path, | ||
189 | token: userToken, | ||
190 | fields, | ||
191 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
192 | }) | ||
193 | }) | ||
194 | |||
195 | it('Should fail with a wrong account', async function () { | ||
196 | const fields = { account: { id: 'blabla' }, reason: 'my super reason' } | ||
197 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
198 | }) | ||
199 | |||
200 | it('Should fail with an unknown account', async function () { | ||
201 | const fields = { account: { id: 42 }, reason: 'my super reason' } | ||
202 | await makePostBodyRequest({ | ||
203 | url: server.url, | ||
204 | path, | ||
205 | token: userToken, | ||
206 | fields, | ||
207 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
208 | }) | ||
209 | }) | ||
210 | |||
211 | it('Should fail with not account, comment or video', async function () { | ||
212 | const fields = { reason: 'my super reason' } | ||
213 | await makePostBodyRequest({ | ||
214 | url: server.url, | ||
215 | path, | ||
216 | token: userToken, | ||
217 | fields, | ||
218 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
219 | }) | ||
220 | }) | ||
221 | |||
222 | it('Should fail with a non authenticated user', async function () { | ||
223 | const fields = { video: { id: server.store.videoCreated.id }, reason: 'my super reason' } | ||
224 | |||
225 | await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
226 | }) | ||
227 | |||
228 | it('Should fail with a reason too short', async function () { | ||
229 | const fields = { video: { id: server.store.videoCreated.id }, reason: 'h' } | ||
230 | |||
231 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
232 | }) | ||
233 | |||
234 | it('Should fail with a too big reason', async function () { | ||
235 | const fields = { video: { id: server.store.videoCreated.id }, reason: 'super'.repeat(605) } | ||
236 | |||
237 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
238 | }) | ||
239 | |||
240 | it('Should succeed with the correct parameters (basic)', async function () { | ||
241 | const fields: AbuseCreate = { video: { id: server.store.videoCreated.shortUUID }, reason: 'my super reason' } | ||
242 | |||
243 | const res = await makePostBodyRequest({ | ||
244 | url: server.url, | ||
245 | path, | ||
246 | token: userToken, | ||
247 | fields, | ||
248 | expectedStatus: HttpStatusCode.OK_200 | ||
249 | }) | ||
250 | abuseId = res.body.abuse.id | ||
251 | }) | ||
252 | |||
253 | it('Should fail with a wrong predefined reason', async function () { | ||
254 | const fields = { video: server.store.videoCreated, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] } | ||
255 | |||
256 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
257 | }) | ||
258 | |||
259 | it('Should fail with negative timestamps', async function () { | ||
260 | const fields = { video: { id: server.store.videoCreated.id, startAt: -1 }, reason: 'my super reason' } | ||
261 | |||
262 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
263 | }) | ||
264 | |||
265 | it('Should fail mith misordered startAt/endAt', async function () { | ||
266 | const fields = { video: { id: server.store.videoCreated.id, startAt: 5, endAt: 1 }, reason: 'my super reason' } | ||
267 | |||
268 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) | ||
269 | }) | ||
270 | |||
271 | it('Should succeed with the correct parameters (advanced)', async function () { | ||
272 | const fields: AbuseCreate = { | ||
273 | video: { | ||
274 | id: server.store.videoCreated.id, | ||
275 | startAt: 1, | ||
276 | endAt: 5 | ||
277 | }, | ||
278 | reason: 'my super reason', | ||
279 | predefinedReasons: [ 'serverRules' ] | ||
280 | } | ||
281 | |||
282 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.OK_200 }) | ||
283 | }) | ||
284 | }) | ||
285 | |||
286 | describe('When updating an abuse', function () { | ||
287 | |||
288 | it('Should fail with a non authenticated user', async function () { | ||
289 | await command.update({ token: 'blabla', abuseId, body: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
290 | }) | ||
291 | |||
292 | it('Should fail with a non admin user', async function () { | ||
293 | await command.update({ token: userToken, abuseId, body: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
294 | }) | ||
295 | |||
296 | it('Should fail with a bad abuse id', async function () { | ||
297 | await command.update({ abuseId: 45, body: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
298 | }) | ||
299 | |||
300 | it('Should fail with a bad state', async function () { | ||
301 | const body = { state: 5 as any } | ||
302 | await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
303 | }) | ||
304 | |||
305 | it('Should fail with a bad moderation comment', async function () { | ||
306 | const body = { moderationComment: 'b'.repeat(3001) } | ||
307 | await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
308 | }) | ||
309 | |||
310 | it('Should succeed with the correct params', async function () { | ||
311 | const body = { state: AbuseState.ACCEPTED } | ||
312 | await command.update({ abuseId, body }) | ||
313 | }) | ||
314 | }) | ||
315 | |||
316 | describe('When creating an abuse message', function () { | ||
317 | const message = 'my super message' | ||
318 | |||
319 | it('Should fail with an invalid abuse id', async function () { | ||
320 | await command.addMessage({ token: userToken2, abuseId: 888, message, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
321 | }) | ||
322 | |||
323 | it('Should fail with a non authenticated user', async function () { | ||
324 | await command.addMessage({ token: 'fake_token', abuseId, message, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
325 | }) | ||
326 | |||
327 | it('Should fail with an invalid logged in user', async function () { | ||
328 | await command.addMessage({ token: userToken2, abuseId, message, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
329 | }) | ||
330 | |||
331 | it('Should fail with an invalid message', async function () { | ||
332 | await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
333 | }) | ||
334 | |||
335 | it('Should succeed with the correct params', async function () { | ||
336 | const res = await command.addMessage({ token: userToken, abuseId, message }) | ||
337 | messageId = res.body.abuseMessage.id | ||
338 | }) | ||
339 | }) | ||
340 | |||
341 | describe('When listing abuse messages', function () { | ||
342 | |||
343 | it('Should fail with an invalid abuse id', async function () { | ||
344 | await command.listMessages({ token: userToken, abuseId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
345 | }) | ||
346 | |||
347 | it('Should fail with a non authenticated user', async function () { | ||
348 | await command.listMessages({ token: 'fake_token', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
349 | }) | ||
350 | |||
351 | it('Should fail with an invalid logged in user', async function () { | ||
352 | await command.listMessages({ token: userToken2, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
353 | }) | ||
354 | |||
355 | it('Should succeed with the correct params', async function () { | ||
356 | await command.listMessages({ token: userToken, abuseId }) | ||
357 | }) | ||
358 | }) | ||
359 | |||
360 | describe('When deleting an abuse message', function () { | ||
361 | it('Should fail with an invalid abuse id', async function () { | ||
362 | await command.deleteMessage({ token: userToken, abuseId: 888, messageId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
363 | }) | ||
364 | |||
365 | it('Should fail with an invalid message id', async function () { | ||
366 | await command.deleteMessage({ token: userToken, abuseId, messageId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
367 | }) | ||
368 | |||
369 | it('Should fail with a non authenticated user', async function () { | ||
370 | await command.deleteMessage({ token: 'fake_token', abuseId, messageId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
371 | }) | ||
372 | |||
373 | it('Should fail with an invalid logged in user', async function () { | ||
374 | await command.deleteMessage({ token: userToken2, abuseId, messageId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
375 | }) | ||
376 | |||
377 | it('Should succeed with the correct params', async function () { | ||
378 | await command.deleteMessage({ token: userToken, abuseId, messageId }) | ||
379 | }) | ||
380 | }) | ||
381 | |||
382 | describe('When deleting a video abuse', function () { | ||
383 | |||
384 | it('Should fail with a non authenticated user', async function () { | ||
385 | await command.delete({ token: 'blabla', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
386 | }) | ||
387 | |||
388 | it('Should fail with a non admin user', async function () { | ||
389 | await command.delete({ token: userToken, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
390 | }) | ||
391 | |||
392 | it('Should fail with a bad abuse id', async function () { | ||
393 | await command.delete({ abuseId: 45, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
394 | }) | ||
395 | |||
396 | it('Should succeed with the correct params', async function () { | ||
397 | await command.delete({ abuseId }) | ||
398 | }) | ||
399 | }) | ||
400 | |||
401 | describe('When trying to manage messages of a remote abuse', function () { | ||
402 | let remoteAbuseId: number | ||
403 | let anotherServer: PeerTubeServer | ||
404 | |||
405 | before(async function () { | ||
406 | this.timeout(50000) | ||
407 | |||
408 | anotherServer = await createSingleServer(2) | ||
409 | await setAccessTokensToServers([ anotherServer ]) | ||
410 | |||
411 | await doubleFollow(anotherServer, server) | ||
412 | |||
413 | const server2VideoId = await anotherServer.videos.getId({ uuid: server.store.videoCreated.uuid }) | ||
414 | await anotherServer.abuses.report({ reason: 'remote server', videoId: server2VideoId }) | ||
415 | |||
416 | await waitJobs([ server, anotherServer ]) | ||
417 | |||
418 | const body = await command.getAdminList({ sort: '-createdAt' }) | ||
419 | remoteAbuseId = body.data[0].id | ||
420 | }) | ||
421 | |||
422 | it('Should fail when listing abuse messages of a remote abuse', async function () { | ||
423 | await command.listMessages({ abuseId: remoteAbuseId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
424 | }) | ||
425 | |||
426 | it('Should fail when creating abuse message of a remote abuse', async function () { | ||
427 | await command.addMessage({ abuseId: remoteAbuseId, message: 'message', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
428 | }) | ||
429 | |||
430 | after(async function () { | ||
431 | await cleanupTests([ anotherServer ]) | ||
432 | }) | ||
433 | }) | ||
434 | |||
435 | after(async function () { | ||
436 | await cleanupTests([ server ]) | ||
437 | }) | ||
438 | }) | ||
diff --git a/packages/tests/src/api/check-params/accounts.ts b/packages/tests/src/api/check-params/accounts.ts new file mode 100644 index 000000000..87810bbd3 --- /dev/null +++ b/packages/tests/src/api/check-params/accounts.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
6 | |||
7 | describe('Test accounts API validators', function () { | ||
8 | const path = '/api/v1/accounts/' | ||
9 | let server: PeerTubeServer | ||
10 | |||
11 | // --------------------------------------------------------------- | ||
12 | |||
13 | before(async function () { | ||
14 | this.timeout(30000) | ||
15 | |||
16 | server = await createSingleServer(1) | ||
17 | }) | ||
18 | |||
19 | describe('When listing accounts', function () { | ||
20 | it('Should fail with a bad start pagination', async function () { | ||
21 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
22 | }) | ||
23 | |||
24 | it('Should fail with a bad count pagination', async function () { | ||
25 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
26 | }) | ||
27 | |||
28 | it('Should fail with an incorrect sort', async function () { | ||
29 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
30 | }) | ||
31 | }) | ||
32 | |||
33 | describe('When getting an account', function () { | ||
34 | |||
35 | it('Should return 404 with a non existing name', async function () { | ||
36 | await server.accounts.get({ accountName: 'arfaze', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
37 | }) | ||
38 | }) | ||
39 | |||
40 | after(async function () { | ||
41 | await cleanupTests([ server ]) | ||
42 | }) | ||
43 | }) | ||
diff --git a/packages/tests/src/api/check-params/blocklist.ts b/packages/tests/src/api/check-params/blocklist.ts new file mode 100644 index 000000000..fcd6d08f8 --- /dev/null +++ b/packages/tests/src/api/check-params/blocklist.ts | |||
@@ -0,0 +1,556 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | makeDeleteRequest, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Test blocklist API validators', function () { | ||
17 | let servers: PeerTubeServer[] | ||
18 | let server: PeerTubeServer | ||
19 | let userAccessToken: string | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(60000) | ||
23 | |||
24 | servers = await createMultipleServers(2) | ||
25 | await setAccessTokensToServers(servers) | ||
26 | |||
27 | server = servers[0] | ||
28 | |||
29 | const user = { username: 'user1', password: 'password' } | ||
30 | await server.users.create({ username: user.username, password: user.password }) | ||
31 | |||
32 | userAccessToken = await server.login.getAccessToken(user) | ||
33 | |||
34 | await doubleFollow(servers[0], servers[1]) | ||
35 | }) | ||
36 | |||
37 | // --------------------------------------------------------------- | ||
38 | |||
39 | describe('When managing user blocklist', function () { | ||
40 | |||
41 | describe('When managing user accounts blocklist', function () { | ||
42 | const path = '/api/v1/users/me/blocklist/accounts' | ||
43 | |||
44 | describe('When listing blocked accounts', function () { | ||
45 | it('Should fail with an unauthenticated user', async function () { | ||
46 | await makeGetRequest({ | ||
47 | url: server.url, | ||
48 | path, | ||
49 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
50 | }) | ||
51 | }) | ||
52 | |||
53 | it('Should fail with a bad start pagination', async function () { | ||
54 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
55 | }) | ||
56 | |||
57 | it('Should fail with a bad count pagination', async function () { | ||
58 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
59 | }) | ||
60 | |||
61 | it('Should fail with an incorrect sort', async function () { | ||
62 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
63 | }) | ||
64 | }) | ||
65 | |||
66 | describe('When blocking an account', function () { | ||
67 | it('Should fail with an unauthenticated user', async function () { | ||
68 | await makePostBodyRequest({ | ||
69 | url: server.url, | ||
70 | path, | ||
71 | fields: { accountName: 'user1' }, | ||
72 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
73 | }) | ||
74 | }) | ||
75 | |||
76 | it('Should fail with an unknown account', async function () { | ||
77 | await makePostBodyRequest({ | ||
78 | url: server.url, | ||
79 | token: server.accessToken, | ||
80 | path, | ||
81 | fields: { accountName: 'user2' }, | ||
82 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
83 | }) | ||
84 | }) | ||
85 | |||
86 | it('Should fail to block ourselves', async function () { | ||
87 | await makePostBodyRequest({ | ||
88 | url: server.url, | ||
89 | token: server.accessToken, | ||
90 | path, | ||
91 | fields: { accountName: 'root' }, | ||
92 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
93 | }) | ||
94 | }) | ||
95 | |||
96 | it('Should succeed with the correct params', async function () { | ||
97 | await makePostBodyRequest({ | ||
98 | url: server.url, | ||
99 | token: server.accessToken, | ||
100 | path, | ||
101 | fields: { accountName: 'user1' }, | ||
102 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
103 | }) | ||
104 | }) | ||
105 | }) | ||
106 | |||
107 | describe('When unblocking an account', function () { | ||
108 | it('Should fail with an unauthenticated user', async function () { | ||
109 | await makeDeleteRequest({ | ||
110 | url: server.url, | ||
111 | path: path + '/user1', | ||
112 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
113 | }) | ||
114 | }) | ||
115 | |||
116 | it('Should fail with an unknown account block', async function () { | ||
117 | await makeDeleteRequest({ | ||
118 | url: server.url, | ||
119 | path: path + '/user2', | ||
120 | token: server.accessToken, | ||
121 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
122 | }) | ||
123 | }) | ||
124 | |||
125 | it('Should succeed with the correct params', async function () { | ||
126 | await makeDeleteRequest({ | ||
127 | url: server.url, | ||
128 | path: path + '/user1', | ||
129 | token: server.accessToken, | ||
130 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
131 | }) | ||
132 | }) | ||
133 | }) | ||
134 | }) | ||
135 | |||
136 | describe('When managing user servers blocklist', function () { | ||
137 | const path = '/api/v1/users/me/blocklist/servers' | ||
138 | |||
139 | describe('When listing blocked servers', function () { | ||
140 | it('Should fail with an unauthenticated user', async function () { | ||
141 | await makeGetRequest({ | ||
142 | url: server.url, | ||
143 | path, | ||
144 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
145 | }) | ||
146 | }) | ||
147 | |||
148 | it('Should fail with a bad start pagination', async function () { | ||
149 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
150 | }) | ||
151 | |||
152 | it('Should fail with a bad count pagination', async function () { | ||
153 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
154 | }) | ||
155 | |||
156 | it('Should fail with an incorrect sort', async function () { | ||
157 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
158 | }) | ||
159 | }) | ||
160 | |||
161 | describe('When blocking a server', function () { | ||
162 | it('Should fail with an unauthenticated user', async function () { | ||
163 | await makePostBodyRequest({ | ||
164 | url: server.url, | ||
165 | path, | ||
166 | fields: { host: '127.0.0.1:9002' }, | ||
167 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
168 | }) | ||
169 | }) | ||
170 | |||
171 | it('Should succeed with an unknown server', async function () { | ||
172 | await makePostBodyRequest({ | ||
173 | url: server.url, | ||
174 | token: server.accessToken, | ||
175 | path, | ||
176 | fields: { host: '127.0.0.1:9003' }, | ||
177 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
178 | }) | ||
179 | }) | ||
180 | |||
181 | it('Should fail with our own server', async function () { | ||
182 | await makePostBodyRequest({ | ||
183 | url: server.url, | ||
184 | token: server.accessToken, | ||
185 | path, | ||
186 | fields: { host: server.host }, | ||
187 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
188 | }) | ||
189 | }) | ||
190 | |||
191 | it('Should succeed with the correct params', async function () { | ||
192 | await makePostBodyRequest({ | ||
193 | url: server.url, | ||
194 | token: server.accessToken, | ||
195 | path, | ||
196 | fields: { host: servers[1].host }, | ||
197 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
198 | }) | ||
199 | }) | ||
200 | }) | ||
201 | |||
202 | describe('When unblocking a server', function () { | ||
203 | it('Should fail with an unauthenticated user', async function () { | ||
204 | await makeDeleteRequest({ | ||
205 | url: server.url, | ||
206 | path: path + '/' + servers[1].host, | ||
207 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
208 | }) | ||
209 | }) | ||
210 | |||
211 | it('Should fail with an unknown server block', async function () { | ||
212 | await makeDeleteRequest({ | ||
213 | url: server.url, | ||
214 | path: path + '/127.0.0.1:9004', | ||
215 | token: server.accessToken, | ||
216 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
217 | }) | ||
218 | }) | ||
219 | |||
220 | it('Should succeed with the correct params', async function () { | ||
221 | await makeDeleteRequest({ | ||
222 | url: server.url, | ||
223 | path: path + '/' + servers[1].host, | ||
224 | token: server.accessToken, | ||
225 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
226 | }) | ||
227 | }) | ||
228 | }) | ||
229 | }) | ||
230 | }) | ||
231 | |||
232 | describe('When managing server blocklist', function () { | ||
233 | |||
234 | describe('When managing server accounts blocklist', function () { | ||
235 | const path = '/api/v1/server/blocklist/accounts' | ||
236 | |||
237 | describe('When listing blocked accounts', function () { | ||
238 | it('Should fail with an unauthenticated user', async function () { | ||
239 | await makeGetRequest({ | ||
240 | url: server.url, | ||
241 | path, | ||
242 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
243 | }) | ||
244 | }) | ||
245 | |||
246 | it('Should fail with a user without the appropriate rights', async function () { | ||
247 | await makeGetRequest({ | ||
248 | url: server.url, | ||
249 | token: userAccessToken, | ||
250 | path, | ||
251 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
252 | }) | ||
253 | }) | ||
254 | |||
255 | it('Should fail with a bad start pagination', async function () { | ||
256 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
257 | }) | ||
258 | |||
259 | it('Should fail with a bad count pagination', async function () { | ||
260 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
261 | }) | ||
262 | |||
263 | it('Should fail with an incorrect sort', async function () { | ||
264 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
265 | }) | ||
266 | }) | ||
267 | |||
268 | describe('When blocking an account', function () { | ||
269 | it('Should fail with an unauthenticated user', async function () { | ||
270 | await makePostBodyRequest({ | ||
271 | url: server.url, | ||
272 | path, | ||
273 | fields: { accountName: 'user1' }, | ||
274 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
275 | }) | ||
276 | }) | ||
277 | |||
278 | it('Should fail with a user without the appropriate rights', async function () { | ||
279 | await makePostBodyRequest({ | ||
280 | url: server.url, | ||
281 | token: userAccessToken, | ||
282 | path, | ||
283 | fields: { accountName: 'user1' }, | ||
284 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
285 | }) | ||
286 | }) | ||
287 | |||
288 | it('Should fail with an unknown account', async function () { | ||
289 | await makePostBodyRequest({ | ||
290 | url: server.url, | ||
291 | token: server.accessToken, | ||
292 | path, | ||
293 | fields: { accountName: 'user2' }, | ||
294 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
295 | }) | ||
296 | }) | ||
297 | |||
298 | it('Should fail to block ourselves', async function () { | ||
299 | await makePostBodyRequest({ | ||
300 | url: server.url, | ||
301 | token: server.accessToken, | ||
302 | path, | ||
303 | fields: { accountName: 'root' }, | ||
304 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
305 | }) | ||
306 | }) | ||
307 | |||
308 | it('Should succeed with the correct params', async function () { | ||
309 | await makePostBodyRequest({ | ||
310 | url: server.url, | ||
311 | token: server.accessToken, | ||
312 | path, | ||
313 | fields: { accountName: 'user1' }, | ||
314 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
315 | }) | ||
316 | }) | ||
317 | }) | ||
318 | |||
319 | describe('When unblocking an account', function () { | ||
320 | it('Should fail with an unauthenticated user', async function () { | ||
321 | await makeDeleteRequest({ | ||
322 | url: server.url, | ||
323 | path: path + '/user1', | ||
324 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
325 | }) | ||
326 | }) | ||
327 | |||
328 | it('Should fail with a user without the appropriate rights', async function () { | ||
329 | await makeDeleteRequest({ | ||
330 | url: server.url, | ||
331 | path: path + '/user1', | ||
332 | token: userAccessToken, | ||
333 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
334 | }) | ||
335 | }) | ||
336 | |||
337 | it('Should fail with an unknown account block', async function () { | ||
338 | await makeDeleteRequest({ | ||
339 | url: server.url, | ||
340 | path: path + '/user2', | ||
341 | token: server.accessToken, | ||
342 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
343 | }) | ||
344 | }) | ||
345 | |||
346 | it('Should succeed with the correct params', async function () { | ||
347 | await makeDeleteRequest({ | ||
348 | url: server.url, | ||
349 | path: path + '/user1', | ||
350 | token: server.accessToken, | ||
351 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
352 | }) | ||
353 | }) | ||
354 | }) | ||
355 | }) | ||
356 | |||
357 | describe('When managing server servers blocklist', function () { | ||
358 | const path = '/api/v1/server/blocklist/servers' | ||
359 | |||
360 | describe('When listing blocked servers', function () { | ||
361 | it('Should fail with an unauthenticated user', async function () { | ||
362 | await makeGetRequest({ | ||
363 | url: server.url, | ||
364 | path, | ||
365 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
366 | }) | ||
367 | }) | ||
368 | |||
369 | it('Should fail with a user without the appropriate rights', async function () { | ||
370 | await makeGetRequest({ | ||
371 | url: server.url, | ||
372 | token: userAccessToken, | ||
373 | path, | ||
374 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
375 | }) | ||
376 | }) | ||
377 | |||
378 | it('Should fail with a bad start pagination', async function () { | ||
379 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
380 | }) | ||
381 | |||
382 | it('Should fail with a bad count pagination', async function () { | ||
383 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
384 | }) | ||
385 | |||
386 | it('Should fail with an incorrect sort', async function () { | ||
387 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
388 | }) | ||
389 | }) | ||
390 | |||
391 | describe('When blocking a server', function () { | ||
392 | it('Should fail with an unauthenticated user', async function () { | ||
393 | await makePostBodyRequest({ | ||
394 | url: server.url, | ||
395 | path, | ||
396 | fields: { host: servers[1].host }, | ||
397 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
398 | }) | ||
399 | }) | ||
400 | |||
401 | it('Should fail with a user without the appropriate rights', async function () { | ||
402 | await makePostBodyRequest({ | ||
403 | url: server.url, | ||
404 | token: userAccessToken, | ||
405 | path, | ||
406 | fields: { host: servers[1].host }, | ||
407 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
408 | }) | ||
409 | }) | ||
410 | |||
411 | it('Should succeed with an unknown server', async function () { | ||
412 | await makePostBodyRequest({ | ||
413 | url: server.url, | ||
414 | token: server.accessToken, | ||
415 | path, | ||
416 | fields: { host: '127.0.0.1:9003' }, | ||
417 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
418 | }) | ||
419 | }) | ||
420 | |||
421 | it('Should fail with our own server', async function () { | ||
422 | await makePostBodyRequest({ | ||
423 | url: server.url, | ||
424 | token: server.accessToken, | ||
425 | path, | ||
426 | fields: { host: server.host }, | ||
427 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
428 | }) | ||
429 | }) | ||
430 | |||
431 | it('Should succeed with the correct params', async function () { | ||
432 | await makePostBodyRequest({ | ||
433 | url: server.url, | ||
434 | token: server.accessToken, | ||
435 | path, | ||
436 | fields: { host: servers[1].host }, | ||
437 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
438 | }) | ||
439 | }) | ||
440 | }) | ||
441 | |||
442 | describe('When unblocking a server', function () { | ||
443 | it('Should fail with an unauthenticated user', async function () { | ||
444 | await makeDeleteRequest({ | ||
445 | url: server.url, | ||
446 | path: path + '/' + servers[1].host, | ||
447 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
448 | }) | ||
449 | }) | ||
450 | |||
451 | it('Should fail with a user without the appropriate rights', async function () { | ||
452 | await makeDeleteRequest({ | ||
453 | url: server.url, | ||
454 | path: path + '/' + servers[1].host, | ||
455 | token: userAccessToken, | ||
456 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
457 | }) | ||
458 | }) | ||
459 | |||
460 | it('Should fail with an unknown server block', async function () { | ||
461 | await makeDeleteRequest({ | ||
462 | url: server.url, | ||
463 | path: path + '/127.0.0.1:9004', | ||
464 | token: server.accessToken, | ||
465 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
466 | }) | ||
467 | }) | ||
468 | |||
469 | it('Should succeed with the correct params', async function () { | ||
470 | await makeDeleteRequest({ | ||
471 | url: server.url, | ||
472 | path: path + '/' + servers[1].host, | ||
473 | token: server.accessToken, | ||
474 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
475 | }) | ||
476 | }) | ||
477 | }) | ||
478 | }) | ||
479 | }) | ||
480 | |||
481 | describe('When getting blocklist status', function () { | ||
482 | const path = '/api/v1/blocklist/status' | ||
483 | |||
484 | it('Should fail with a bad token', async function () { | ||
485 | await makeGetRequest({ | ||
486 | url: server.url, | ||
487 | path, | ||
488 | token: 'false', | ||
489 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
490 | }) | ||
491 | }) | ||
492 | |||
493 | it('Should fail with a bad accounts field', async function () { | ||
494 | await makeGetRequest({ | ||
495 | url: server.url, | ||
496 | path, | ||
497 | query: { | ||
498 | accounts: 1 | ||
499 | }, | ||
500 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
501 | }) | ||
502 | |||
503 | await makeGetRequest({ | ||
504 | url: server.url, | ||
505 | path, | ||
506 | query: { | ||
507 | accounts: [ 1 ] | ||
508 | }, | ||
509 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
510 | }) | ||
511 | }) | ||
512 | |||
513 | it('Should fail with a bad hosts field', async function () { | ||
514 | await makeGetRequest({ | ||
515 | url: server.url, | ||
516 | path, | ||
517 | query: { | ||
518 | hosts: 1 | ||
519 | }, | ||
520 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
521 | }) | ||
522 | |||
523 | await makeGetRequest({ | ||
524 | url: server.url, | ||
525 | path, | ||
526 | query: { | ||
527 | hosts: [ 1 ] | ||
528 | }, | ||
529 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
530 | }) | ||
531 | }) | ||
532 | |||
533 | it('Should succeed with the correct parameters', async function () { | ||
534 | await makeGetRequest({ | ||
535 | url: server.url, | ||
536 | path, | ||
537 | query: {}, | ||
538 | expectedStatus: HttpStatusCode.OK_200 | ||
539 | }) | ||
540 | |||
541 | await makeGetRequest({ | ||
542 | url: server.url, | ||
543 | path, | ||
544 | query: { | ||
545 | hosts: [ 'example.com' ], | ||
546 | accounts: [ 'john@example.com' ] | ||
547 | }, | ||
548 | expectedStatus: HttpStatusCode.OK_200 | ||
549 | }) | ||
550 | }) | ||
551 | }) | ||
552 | |||
553 | after(async function () { | ||
554 | await cleanupTests(servers) | ||
555 | }) | ||
556 | }) | ||
diff --git a/packages/tests/src/api/check-params/bulk.ts b/packages/tests/src/api/check-params/bulk.ts new file mode 100644 index 000000000..def0c38eb --- /dev/null +++ b/packages/tests/src/api/check-params/bulk.ts | |||
@@ -0,0 +1,86 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makePostBodyRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers | ||
10 | } from '@peertube/peertube-server-commands' | ||
11 | |||
12 | describe('Test bulk API validators', function () { | ||
13 | let server: PeerTubeServer | ||
14 | let userAccessToken: string | ||
15 | |||
16 | // --------------------------------------------------------------- | ||
17 | |||
18 | before(async function () { | ||
19 | this.timeout(120000) | ||
20 | |||
21 | server = await createSingleServer(1) | ||
22 | await setAccessTokensToServers([ server ]) | ||
23 | |||
24 | const user = { username: 'user1', password: 'password' } | ||
25 | await server.users.create({ username: user.username, password: user.password }) | ||
26 | |||
27 | userAccessToken = await server.login.getAccessToken(user) | ||
28 | }) | ||
29 | |||
30 | describe('When removing comments of', function () { | ||
31 | const path = '/api/v1/bulk/remove-comments-of' | ||
32 | |||
33 | it('Should fail with an unauthenticated user', async function () { | ||
34 | await makePostBodyRequest({ | ||
35 | url: server.url, | ||
36 | path, | ||
37 | fields: { accountName: 'user1', scope: 'my-videos' }, | ||
38 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
39 | }) | ||
40 | }) | ||
41 | |||
42 | it('Should fail with an unknown account', async function () { | ||
43 | await makePostBodyRequest({ | ||
44 | url: server.url, | ||
45 | token: server.accessToken, | ||
46 | path, | ||
47 | fields: { accountName: 'user2', scope: 'my-videos' }, | ||
48 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
49 | }) | ||
50 | }) | ||
51 | |||
52 | it('Should fail with an invalid scope', async function () { | ||
53 | await makePostBodyRequest({ | ||
54 | url: server.url, | ||
55 | token: server.accessToken, | ||
56 | path, | ||
57 | fields: { accountName: 'user1', scope: 'my-videoss' }, | ||
58 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | it('Should fail to delete comments of the instance without the appropriate rights', async function () { | ||
63 | await makePostBodyRequest({ | ||
64 | url: server.url, | ||
65 | token: userAccessToken, | ||
66 | path, | ||
67 | fields: { accountName: 'user1', scope: 'instance' }, | ||
68 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
69 | }) | ||
70 | }) | ||
71 | |||
72 | it('Should succeed with the correct params', async function () { | ||
73 | await makePostBodyRequest({ | ||
74 | url: server.url, | ||
75 | token: server.accessToken, | ||
76 | path, | ||
77 | fields: { accountName: 'user1', scope: 'instance' }, | ||
78 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
79 | }) | ||
80 | }) | ||
81 | }) | ||
82 | |||
83 | after(async function () { | ||
84 | await cleanupTests([ server ]) | ||
85 | }) | ||
86 | }) | ||
diff --git a/packages/tests/src/api/check-params/channel-import-videos.ts b/packages/tests/src/api/check-params/channel-import-videos.ts new file mode 100644 index 000000000..0e897dad7 --- /dev/null +++ b/packages/tests/src/api/check-params/channel-import-videos.ts | |||
@@ -0,0 +1,209 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
4 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | ChannelsCommand, | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test videos import in a channel API validator', function () { | ||
16 | let server: PeerTubeServer | ||
17 | const userInfo = { | ||
18 | accessToken: '', | ||
19 | channelName: 'fake_channel', | ||
20 | channelId: -1, | ||
21 | id: -1, | ||
22 | videoQuota: -1, | ||
23 | videoQuotaDaily: -1, | ||
24 | channelSyncId: -1 | ||
25 | } | ||
26 | let command: ChannelsCommand | ||
27 | |||
28 | // --------------------------------------------------------------- | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120000) | ||
32 | |||
33 | server = await createSingleServer(1) | ||
34 | |||
35 | await setAccessTokensToServers([ server ]) | ||
36 | await setDefaultVideoChannel([ server ]) | ||
37 | |||
38 | await server.config.enableImports() | ||
39 | await server.config.enableChannelSync() | ||
40 | |||
41 | const userCreds = { | ||
42 | username: 'fake', | ||
43 | password: 'fake_password' | ||
44 | } | ||
45 | |||
46 | { | ||
47 | const user = await server.users.create({ username: userCreds.username, password: userCreds.password }) | ||
48 | userInfo.id = user.id | ||
49 | userInfo.accessToken = await server.login.getAccessToken(userCreds) | ||
50 | |||
51 | const info = await server.users.getMyInfo({ token: userInfo.accessToken }) | ||
52 | userInfo.channelId = info.videoChannels[0].id | ||
53 | } | ||
54 | |||
55 | { | ||
56 | const { videoChannelSync } = await server.channelSyncs.create({ | ||
57 | token: userInfo.accessToken, | ||
58 | attributes: { | ||
59 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
60 | videoChannelId: userInfo.channelId | ||
61 | } | ||
62 | }) | ||
63 | userInfo.channelSyncId = videoChannelSync.id | ||
64 | } | ||
65 | |||
66 | command = server.channels | ||
67 | }) | ||
68 | |||
69 | it('Should fail when HTTP upload is disabled', async function () { | ||
70 | await server.config.disableChannelSync() | ||
71 | await server.config.disableImports() | ||
72 | |||
73 | await command.importVideos({ | ||
74 | channelName: server.store.channel.name, | ||
75 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
76 | token: server.accessToken, | ||
77 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
78 | }) | ||
79 | |||
80 | await server.config.enableImports() | ||
81 | }) | ||
82 | |||
83 | it('Should fail when externalChannelUrl is not provided', async function () { | ||
84 | await command.importVideos({ | ||
85 | channelName: server.store.channel.name, | ||
86 | externalChannelUrl: null, | ||
87 | token: server.accessToken, | ||
88 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
89 | }) | ||
90 | }) | ||
91 | |||
92 | it('Should fail when externalChannelUrl is malformed', async function () { | ||
93 | await command.importVideos({ | ||
94 | channelName: server.store.channel.name, | ||
95 | externalChannelUrl: 'not-a-url', | ||
96 | token: server.accessToken, | ||
97 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
98 | }) | ||
99 | }) | ||
100 | |||
101 | it('Should fail with a bad sync id', async function () { | ||
102 | await command.importVideos({ | ||
103 | channelName: server.store.channel.name, | ||
104 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
105 | videoChannelSyncId: 'toto' as any, | ||
106 | token: server.accessToken, | ||
107 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
108 | }) | ||
109 | }) | ||
110 | |||
111 | it('Should fail with a unknown sync id', async function () { | ||
112 | await command.importVideos({ | ||
113 | channelName: server.store.channel.name, | ||
114 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
115 | videoChannelSyncId: 42, | ||
116 | token: server.accessToken, | ||
117 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
118 | }) | ||
119 | }) | ||
120 | |||
121 | it('Should fail with a sync id of another channel', async function () { | ||
122 | await command.importVideos({ | ||
123 | channelName: server.store.channel.name, | ||
124 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
125 | videoChannelSyncId: userInfo.channelSyncId, | ||
126 | token: server.accessToken, | ||
127 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
128 | }) | ||
129 | }) | ||
130 | |||
131 | it('Should fail with no authentication', async function () { | ||
132 | await command.importVideos({ | ||
133 | channelName: server.store.channel.name, | ||
134 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
135 | token: null, | ||
136 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
137 | }) | ||
138 | }) | ||
139 | |||
140 | it('Should fail when sync is not owned by the user', async function () { | ||
141 | await command.importVideos({ | ||
142 | channelName: server.store.channel.name, | ||
143 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
144 | token: userInfo.accessToken, | ||
145 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
146 | }) | ||
147 | }) | ||
148 | |||
149 | it('Should fail when the user has no quota', async function () { | ||
150 | await server.users.update({ | ||
151 | userId: userInfo.id, | ||
152 | videoQuota: 0 | ||
153 | }) | ||
154 | |||
155 | await command.importVideos({ | ||
156 | channelName: 'fake_channel', | ||
157 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
158 | token: userInfo.accessToken, | ||
159 | expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413 | ||
160 | }) | ||
161 | |||
162 | await server.users.update({ | ||
163 | userId: userInfo.id, | ||
164 | videoQuota: userInfo.videoQuota | ||
165 | }) | ||
166 | }) | ||
167 | |||
168 | it('Should fail when the user has no daily quota', async function () { | ||
169 | await server.users.update({ | ||
170 | userId: userInfo.id, | ||
171 | videoQuotaDaily: 0 | ||
172 | }) | ||
173 | |||
174 | await command.importVideos({ | ||
175 | channelName: 'fake_channel', | ||
176 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
177 | token: userInfo.accessToken, | ||
178 | expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413 | ||
179 | }) | ||
180 | |||
181 | await server.users.update({ | ||
182 | userId: userInfo.id, | ||
183 | videoQuotaDaily: userInfo.videoQuotaDaily | ||
184 | }) | ||
185 | }) | ||
186 | |||
187 | it('Should succeed when sync is run by its owner', async function () { | ||
188 | if (!areHttpImportTestsDisabled()) return | ||
189 | |||
190 | await command.importVideos({ | ||
191 | channelName: 'fake_channel', | ||
192 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
193 | token: userInfo.accessToken | ||
194 | }) | ||
195 | }) | ||
196 | |||
197 | it('Should succeed when sync is run with root and for another user\'s channel', async function () { | ||
198 | if (!areHttpImportTestsDisabled()) return | ||
199 | |||
200 | await command.importVideos({ | ||
201 | channelName: 'fake_channel', | ||
202 | externalChannelUrl: FIXTURE_URLS.youtubeChannel | ||
203 | }) | ||
204 | }) | ||
205 | |||
206 | after(async function () { | ||
207 | await cleanupTests([ server ]) | ||
208 | }) | ||
209 | }) | ||
diff --git a/packages/tests/src/api/check-params/config.ts b/packages/tests/src/api/check-params/config.ts new file mode 100644 index 000000000..8179a8815 --- /dev/null +++ b/packages/tests/src/api/check-params/config.ts | |||
@@ -0,0 +1,428 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import merge from 'lodash-es/merge.js' | ||
3 | import { omit } from '@peertube/peertube-core-utils' | ||
4 | import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeDeleteRequest, | ||
9 | makeGetRequest, | ||
10 | makePutBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test config API validators', function () { | ||
16 | const path = '/api/v1/config/custom' | ||
17 | let server: PeerTubeServer | ||
18 | let userAccessToken: string | ||
19 | const updateParams: CustomConfig = { | ||
20 | instance: { | ||
21 | name: 'PeerTube updated', | ||
22 | shortDescription: 'my short description', | ||
23 | description: 'my super description', | ||
24 | terms: 'my super terms', | ||
25 | codeOfConduct: 'my super coc', | ||
26 | |||
27 | creationReason: 'my super reason', | ||
28 | moderationInformation: 'my super moderation information', | ||
29 | administrator: 'Kuja', | ||
30 | maintenanceLifetime: 'forever', | ||
31 | businessModel: 'my super business model', | ||
32 | hardwareInformation: '2vCore 3GB RAM', | ||
33 | |||
34 | languages: [ 'en', 'es' ], | ||
35 | categories: [ 1, 2 ], | ||
36 | |||
37 | isNSFW: true, | ||
38 | defaultNSFWPolicy: 'blur', | ||
39 | |||
40 | defaultClientRoute: '/videos/recently-added', | ||
41 | |||
42 | customizations: { | ||
43 | javascript: 'alert("coucou")', | ||
44 | css: 'body { background-color: red; }' | ||
45 | } | ||
46 | }, | ||
47 | theme: { | ||
48 | default: 'default' | ||
49 | }, | ||
50 | services: { | ||
51 | twitter: { | ||
52 | username: '@MySuperUsername', | ||
53 | whitelisted: true | ||
54 | } | ||
55 | }, | ||
56 | client: { | ||
57 | videos: { | ||
58 | miniature: { | ||
59 | preferAuthorDisplayName: false | ||
60 | } | ||
61 | }, | ||
62 | menu: { | ||
63 | login: { | ||
64 | redirectOnSingleExternalAuth: false | ||
65 | } | ||
66 | } | ||
67 | }, | ||
68 | cache: { | ||
69 | previews: { | ||
70 | size: 2 | ||
71 | }, | ||
72 | captions: { | ||
73 | size: 3 | ||
74 | }, | ||
75 | torrents: { | ||
76 | size: 4 | ||
77 | }, | ||
78 | storyboards: { | ||
79 | size: 5 | ||
80 | } | ||
81 | }, | ||
82 | signup: { | ||
83 | enabled: false, | ||
84 | limit: 5, | ||
85 | requiresApproval: false, | ||
86 | requiresEmailVerification: false, | ||
87 | minimumAge: 16 | ||
88 | }, | ||
89 | admin: { | ||
90 | email: 'superadmin1@example.com' | ||
91 | }, | ||
92 | contactForm: { | ||
93 | enabled: false | ||
94 | }, | ||
95 | user: { | ||
96 | history: { | ||
97 | videos: { | ||
98 | enabled: true | ||
99 | } | ||
100 | }, | ||
101 | videoQuota: 5242881, | ||
102 | videoQuotaDaily: 318742 | ||
103 | }, | ||
104 | videoChannels: { | ||
105 | maxPerUser: 20 | ||
106 | }, | ||
107 | transcoding: { | ||
108 | enabled: true, | ||
109 | remoteRunners: { | ||
110 | enabled: true | ||
111 | }, | ||
112 | allowAdditionalExtensions: true, | ||
113 | allowAudioFiles: true, | ||
114 | concurrency: 1, | ||
115 | threads: 1, | ||
116 | profile: 'vod_profile', | ||
117 | resolutions: { | ||
118 | '0p': false, | ||
119 | '144p': false, | ||
120 | '240p': false, | ||
121 | '360p': true, | ||
122 | '480p': true, | ||
123 | '720p': false, | ||
124 | '1080p': false, | ||
125 | '1440p': false, | ||
126 | '2160p': false | ||
127 | }, | ||
128 | alwaysTranscodeOriginalResolution: false, | ||
129 | webVideos: { | ||
130 | enabled: true | ||
131 | }, | ||
132 | hls: { | ||
133 | enabled: false | ||
134 | } | ||
135 | }, | ||
136 | live: { | ||
137 | enabled: true, | ||
138 | |||
139 | allowReplay: false, | ||
140 | latencySetting: { | ||
141 | enabled: false | ||
142 | }, | ||
143 | maxDuration: 30, | ||
144 | maxInstanceLives: -1, | ||
145 | maxUserLives: 50, | ||
146 | |||
147 | transcoding: { | ||
148 | enabled: true, | ||
149 | remoteRunners: { | ||
150 | enabled: true | ||
151 | }, | ||
152 | threads: 4, | ||
153 | profile: 'live_profile', | ||
154 | resolutions: { | ||
155 | '144p': true, | ||
156 | '240p': true, | ||
157 | '360p': true, | ||
158 | '480p': true, | ||
159 | '720p': true, | ||
160 | '1080p': true, | ||
161 | '1440p': true, | ||
162 | '2160p': true | ||
163 | }, | ||
164 | alwaysTranscodeOriginalResolution: false | ||
165 | } | ||
166 | }, | ||
167 | videoStudio: { | ||
168 | enabled: true, | ||
169 | remoteRunners: { | ||
170 | enabled: true | ||
171 | } | ||
172 | }, | ||
173 | videoFile: { | ||
174 | update: { | ||
175 | enabled: true | ||
176 | } | ||
177 | }, | ||
178 | import: { | ||
179 | videos: { | ||
180 | concurrency: 1, | ||
181 | http: { | ||
182 | enabled: false | ||
183 | }, | ||
184 | torrent: { | ||
185 | enabled: false | ||
186 | } | ||
187 | }, | ||
188 | videoChannelSynchronization: { | ||
189 | enabled: false, | ||
190 | maxPerUser: 10 | ||
191 | } | ||
192 | }, | ||
193 | trending: { | ||
194 | videos: { | ||
195 | algorithms: { | ||
196 | enabled: [ 'hot', 'most-viewed', 'most-liked' ], | ||
197 | default: 'most-viewed' | ||
198 | } | ||
199 | } | ||
200 | }, | ||
201 | autoBlacklist: { | ||
202 | videos: { | ||
203 | ofUsers: { | ||
204 | enabled: false | ||
205 | } | ||
206 | } | ||
207 | }, | ||
208 | followers: { | ||
209 | instance: { | ||
210 | enabled: false, | ||
211 | manualApproval: true | ||
212 | } | ||
213 | }, | ||
214 | followings: { | ||
215 | instance: { | ||
216 | autoFollowBack: { | ||
217 | enabled: true | ||
218 | }, | ||
219 | autoFollowIndex: { | ||
220 | enabled: true, | ||
221 | indexUrl: 'https://index.example.com' | ||
222 | } | ||
223 | } | ||
224 | }, | ||
225 | broadcastMessage: { | ||
226 | enabled: true, | ||
227 | dismissable: true, | ||
228 | message: 'super message', | ||
229 | level: 'warning' | ||
230 | }, | ||
231 | search: { | ||
232 | remoteUri: { | ||
233 | users: true, | ||
234 | anonymous: true | ||
235 | }, | ||
236 | searchIndex: { | ||
237 | enabled: true, | ||
238 | url: 'https://search.joinpeertube.org', | ||
239 | disableLocalSearch: true, | ||
240 | isDefaultSearch: true | ||
241 | } | ||
242 | } | ||
243 | } | ||
244 | |||
245 | // --------------------------------------------------------------- | ||
246 | |||
247 | before(async function () { | ||
248 | this.timeout(30000) | ||
249 | |||
250 | server = await createSingleServer(1) | ||
251 | |||
252 | await setAccessTokensToServers([ server ]) | ||
253 | |||
254 | const user = { | ||
255 | username: 'user1', | ||
256 | password: 'password' | ||
257 | } | ||
258 | await server.users.create({ username: user.username, password: user.password }) | ||
259 | userAccessToken = await server.login.getAccessToken(user) | ||
260 | }) | ||
261 | |||
262 | describe('When getting the configuration', function () { | ||
263 | it('Should fail without token', async function () { | ||
264 | await makeGetRequest({ | ||
265 | url: server.url, | ||
266 | path, | ||
267 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
268 | }) | ||
269 | }) | ||
270 | |||
271 | it('Should fail if the user is not an administrator', async function () { | ||
272 | await makeGetRequest({ | ||
273 | url: server.url, | ||
274 | path, | ||
275 | token: userAccessToken, | ||
276 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
277 | }) | ||
278 | }) | ||
279 | }) | ||
280 | |||
281 | describe('When updating the configuration', function () { | ||
282 | it('Should fail without token', async function () { | ||
283 | await makePutBodyRequest({ | ||
284 | url: server.url, | ||
285 | path, | ||
286 | fields: updateParams, | ||
287 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
288 | }) | ||
289 | }) | ||
290 | |||
291 | it('Should fail if the user is not an administrator', async function () { | ||
292 | await makePutBodyRequest({ | ||
293 | url: server.url, | ||
294 | path, | ||
295 | fields: updateParams, | ||
296 | token: userAccessToken, | ||
297 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
298 | }) | ||
299 | }) | ||
300 | |||
301 | it('Should fail if it misses a key', async function () { | ||
302 | const newUpdateParams = { ...updateParams, admin: omit(updateParams.admin, [ 'email' ]) } | ||
303 | |||
304 | await makePutBodyRequest({ | ||
305 | url: server.url, | ||
306 | path, | ||
307 | fields: newUpdateParams, | ||
308 | token: server.accessToken, | ||
309 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
310 | }) | ||
311 | }) | ||
312 | |||
313 | it('Should fail with a bad default NSFW policy', async function () { | ||
314 | const newUpdateParams = { | ||
315 | ...updateParams, | ||
316 | |||
317 | instance: { | ||
318 | defaultNSFWPolicy: 'hello' | ||
319 | } | ||
320 | } | ||
321 | |||
322 | await makePutBodyRequest({ | ||
323 | url: server.url, | ||
324 | path, | ||
325 | fields: newUpdateParams, | ||
326 | token: server.accessToken, | ||
327 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
328 | }) | ||
329 | }) | ||
330 | |||
331 | it('Should fail if email disabled and signup requires email verification', async function () { | ||
332 | // opposite scenario - success when enable enabled - covered via tests/api/users/user-verification.ts | ||
333 | const newUpdateParams = { | ||
334 | ...updateParams, | ||
335 | |||
336 | signup: { | ||
337 | enabled: true, | ||
338 | limit: 5, | ||
339 | requiresApproval: true, | ||
340 | requiresEmailVerification: true | ||
341 | } | ||
342 | } | ||
343 | |||
344 | await makePutBodyRequest({ | ||
345 | url: server.url, | ||
346 | path, | ||
347 | fields: newUpdateParams, | ||
348 | token: server.accessToken, | ||
349 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
350 | }) | ||
351 | }) | ||
352 | |||
353 | it('Should fail with a disabled web videos & hls transcoding', async function () { | ||
354 | const newUpdateParams = { | ||
355 | ...updateParams, | ||
356 | |||
357 | transcoding: { | ||
358 | hls: { | ||
359 | enabled: false | ||
360 | }, | ||
361 | web_videos: { | ||
362 | enabled: false | ||
363 | } | ||
364 | } | ||
365 | } | ||
366 | |||
367 | await makePutBodyRequest({ | ||
368 | url: server.url, | ||
369 | path, | ||
370 | fields: newUpdateParams, | ||
371 | token: server.accessToken, | ||
372 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
373 | }) | ||
374 | }) | ||
375 | |||
376 | it('Should fail with a disabled http upload & enabled sync', async function () { | ||
377 | const newUpdateParams: CustomConfig = merge({}, updateParams, { | ||
378 | import: { | ||
379 | videos: { | ||
380 | http: { enabled: false } | ||
381 | }, | ||
382 | videoChannelSynchronization: { enabled: true } | ||
383 | } | ||
384 | }) | ||
385 | |||
386 | await makePutBodyRequest({ | ||
387 | url: server.url, | ||
388 | path, | ||
389 | fields: newUpdateParams, | ||
390 | token: server.accessToken, | ||
391 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
392 | }) | ||
393 | }) | ||
394 | |||
395 | it('Should succeed with the correct parameters', async function () { | ||
396 | await makePutBodyRequest({ | ||
397 | url: server.url, | ||
398 | path, | ||
399 | fields: updateParams, | ||
400 | token: server.accessToken, | ||
401 | expectedStatus: HttpStatusCode.OK_200 | ||
402 | }) | ||
403 | }) | ||
404 | }) | ||
405 | |||
406 | describe('When deleting the configuration', function () { | ||
407 | it('Should fail without token', async function () { | ||
408 | await makeDeleteRequest({ | ||
409 | url: server.url, | ||
410 | path, | ||
411 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
412 | }) | ||
413 | }) | ||
414 | |||
415 | it('Should fail if the user is not an administrator', async function () { | ||
416 | await makeDeleteRequest({ | ||
417 | url: server.url, | ||
418 | path, | ||
419 | token: userAccessToken, | ||
420 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
421 | }) | ||
422 | }) | ||
423 | }) | ||
424 | |||
425 | after(async function () { | ||
426 | await cleanupTests([ server ]) | ||
427 | }) | ||
428 | }) | ||
diff --git a/packages/tests/src/api/check-params/contact-form.ts b/packages/tests/src/api/check-params/contact-form.ts new file mode 100644 index 000000000..009cb2ad9 --- /dev/null +++ b/packages/tests/src/api/check-params/contact-form.ts | |||
@@ -0,0 +1,86 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | ConfigCommand, | ||
8 | ContactFormCommand, | ||
9 | createSingleServer, | ||
10 | killallServers, | ||
11 | PeerTubeServer | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test contact form API validators', function () { | ||
15 | let server: PeerTubeServer | ||
16 | const emails: object[] = [] | ||
17 | const defaultBody = { | ||
18 | fromName: 'super name', | ||
19 | fromEmail: 'toto@example.com', | ||
20 | subject: 'my subject', | ||
21 | body: 'Hello, how are you?' | ||
22 | } | ||
23 | let emailPort: number | ||
24 | let command: ContactFormCommand | ||
25 | |||
26 | // --------------------------------------------------------------- | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(60000) | ||
30 | |||
31 | emailPort = await MockSmtpServer.Instance.collectEmails(emails) | ||
32 | |||
33 | // Email is disabled | ||
34 | server = await createSingleServer(1) | ||
35 | command = server.contactForm | ||
36 | }) | ||
37 | |||
38 | it('Should not accept a contact form if emails are disabled', async function () { | ||
39 | await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
40 | }) | ||
41 | |||
42 | it('Should not accept a contact form if it is disabled in the configuration', async function () { | ||
43 | this.timeout(25000) | ||
44 | |||
45 | await killallServers([ server ]) | ||
46 | |||
47 | // Contact form is disabled | ||
48 | await server.run({ ...ConfigCommand.getEmailOverrideConfig(emailPort), contact_form: { enabled: false } }) | ||
49 | await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
50 | }) | ||
51 | |||
52 | it('Should not accept a contact form if from email is invalid', async function () { | ||
53 | this.timeout(25000) | ||
54 | |||
55 | await killallServers([ server ]) | ||
56 | |||
57 | // Email & contact form enabled | ||
58 | await server.run(ConfigCommand.getEmailOverrideConfig(emailPort)) | ||
59 | |||
60 | await command.send({ ...defaultBody, fromEmail: 'badEmail', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
61 | await command.send({ ...defaultBody, fromEmail: 'badEmail@', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
62 | await command.send({ ...defaultBody, fromEmail: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
63 | }) | ||
64 | |||
65 | it('Should not accept a contact form if from name is invalid', async function () { | ||
66 | await command.send({ ...defaultBody, fromName: 'name'.repeat(100), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
67 | await command.send({ ...defaultBody, fromName: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
68 | await command.send({ ...defaultBody, fromName: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
69 | }) | ||
70 | |||
71 | it('Should not accept a contact form if body is invalid', async function () { | ||
72 | await command.send({ ...defaultBody, body: 'body'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
73 | await command.send({ ...defaultBody, body: 'a', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
74 | await command.send({ ...defaultBody, body: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
75 | }) | ||
76 | |||
77 | it('Should accept a contact form with the correct parameters', async function () { | ||
78 | await command.send(defaultBody) | ||
79 | }) | ||
80 | |||
81 | after(async function () { | ||
82 | MockSmtpServer.Instance.kill() | ||
83 | |||
84 | await cleanupTests([ server ]) | ||
85 | }) | ||
86 | }) | ||
diff --git a/packages/tests/src/api/check-params/custom-pages.ts b/packages/tests/src/api/check-params/custom-pages.ts new file mode 100644 index 000000000..180a5e406 --- /dev/null +++ b/packages/tests/src/api/check-params/custom-pages.ts | |||
@@ -0,0 +1,79 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makeGetRequest, | ||
8 | makePutBodyRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test custom pages validators', function () { | ||
14 | const path = '/api/v1/custom-pages/homepage/instance' | ||
15 | |||
16 | let server: PeerTubeServer | ||
17 | let userAccessToken: string | ||
18 | |||
19 | // --------------------------------------------------------------- | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(120000) | ||
23 | |||
24 | server = await createSingleServer(1) | ||
25 | await setAccessTokensToServers([ server ]) | ||
26 | |||
27 | const user = { username: 'user1', password: 'password' } | ||
28 | await server.users.create({ username: user.username, password: user.password }) | ||
29 | |||
30 | userAccessToken = await server.login.getAccessToken(user) | ||
31 | }) | ||
32 | |||
33 | describe('When updating instance homepage', function () { | ||
34 | |||
35 | it('Should fail with an unauthenticated user', async function () { | ||
36 | await makePutBodyRequest({ | ||
37 | url: server.url, | ||
38 | path, | ||
39 | fields: { content: 'super content' }, | ||
40 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
41 | }) | ||
42 | }) | ||
43 | |||
44 | it('Should fail with a non admin user', async function () { | ||
45 | await makePutBodyRequest({ | ||
46 | url: server.url, | ||
47 | path, | ||
48 | token: userAccessToken, | ||
49 | fields: { content: 'super content' }, | ||
50 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
51 | }) | ||
52 | }) | ||
53 | |||
54 | it('Should succeed with the correct params', async function () { | ||
55 | await makePutBodyRequest({ | ||
56 | url: server.url, | ||
57 | path, | ||
58 | token: server.accessToken, | ||
59 | fields: { content: 'super content' }, | ||
60 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
61 | }) | ||
62 | }) | ||
63 | }) | ||
64 | |||
65 | describe('When getting instance homapage', function () { | ||
66 | |||
67 | it('Should succeed with the correct params', async function () { | ||
68 | await makeGetRequest({ | ||
69 | url: server.url, | ||
70 | path, | ||
71 | expectedStatus: HttpStatusCode.OK_200 | ||
72 | }) | ||
73 | }) | ||
74 | }) | ||
75 | |||
76 | after(async function () { | ||
77 | await cleanupTests([ server ]) | ||
78 | }) | ||
79 | }) | ||
diff --git a/packages/tests/src/api/check-params/debug.ts b/packages/tests/src/api/check-params/debug.ts new file mode 100644 index 000000000..4a7c18a62 --- /dev/null +++ b/packages/tests/src/api/check-params/debug.ts | |||
@@ -0,0 +1,67 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makeGetRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers | ||
10 | } from '@peertube/peertube-server-commands' | ||
11 | |||
12 | describe('Test debug API validators', function () { | ||
13 | const path = '/api/v1/server/debug' | ||
14 | let server: PeerTubeServer | ||
15 | let userAccessToken = '' | ||
16 | |||
17 | // --------------------------------------------------------------- | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(120000) | ||
21 | |||
22 | server = await createSingleServer(1) | ||
23 | |||
24 | await setAccessTokensToServers([ server ]) | ||
25 | |||
26 | const user = { | ||
27 | username: 'user1', | ||
28 | password: 'my super password' | ||
29 | } | ||
30 | await server.users.create({ username: user.username, password: user.password }) | ||
31 | userAccessToken = await server.login.getAccessToken(user) | ||
32 | }) | ||
33 | |||
34 | describe('When getting debug endpoint', function () { | ||
35 | |||
36 | it('Should fail with a non authenticated user', async function () { | ||
37 | await makeGetRequest({ | ||
38 | url: server.url, | ||
39 | path, | ||
40 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
41 | }) | ||
42 | }) | ||
43 | |||
44 | it('Should fail with a non admin user', async function () { | ||
45 | await makeGetRequest({ | ||
46 | url: server.url, | ||
47 | path, | ||
48 | token: userAccessToken, | ||
49 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
50 | }) | ||
51 | }) | ||
52 | |||
53 | it('Should succeed with the correct params', async function () { | ||
54 | await makeGetRequest({ | ||
55 | url: server.url, | ||
56 | path, | ||
57 | token: server.accessToken, | ||
58 | query: { startDate: new Date().toISOString() }, | ||
59 | expectedStatus: HttpStatusCode.OK_200 | ||
60 | }) | ||
61 | }) | ||
62 | }) | ||
63 | |||
64 | after(async function () { | ||
65 | await cleanupTests([ server ]) | ||
66 | }) | ||
67 | }) | ||
diff --git a/packages/tests/src/api/check-params/follows.ts b/packages/tests/src/api/check-params/follows.ts new file mode 100644 index 000000000..e92a3acd6 --- /dev/null +++ b/packages/tests/src/api/check-params/follows.ts | |||
@@ -0,0 +1,369 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeDeleteRequest, | ||
9 | makeGetRequest, | ||
10 | makePostBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test server follows API validators', function () { | ||
16 | let server: PeerTubeServer | ||
17 | |||
18 | // --------------------------------------------------------------- | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(30000) | ||
22 | |||
23 | server = await createSingleServer(1) | ||
24 | |||
25 | await setAccessTokensToServers([ server ]) | ||
26 | }) | ||
27 | |||
28 | describe('When managing following', function () { | ||
29 | let userAccessToken = null | ||
30 | |||
31 | before(async function () { | ||
32 | userAccessToken = await server.users.generateUserAndToken('user1') | ||
33 | }) | ||
34 | |||
35 | describe('When adding follows', function () { | ||
36 | const path = '/api/v1/server/following' | ||
37 | |||
38 | it('Should fail with nothing', async function () { | ||
39 | await makePostBodyRequest({ | ||
40 | url: server.url, | ||
41 | path, | ||
42 | token: server.accessToken, | ||
43 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
44 | }) | ||
45 | }) | ||
46 | |||
47 | it('Should fail if hosts is not composed by hosts', async function () { | ||
48 | await makePostBodyRequest({ | ||
49 | url: server.url, | ||
50 | path, | ||
51 | fields: { hosts: [ '127.0.0.1:9002', '127.0.0.1:coucou' ] }, | ||
52 | token: server.accessToken, | ||
53 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
54 | }) | ||
55 | }) | ||
56 | |||
57 | it('Should fail if hosts is composed with http schemes', async function () { | ||
58 | await makePostBodyRequest({ | ||
59 | url: server.url, | ||
60 | path, | ||
61 | fields: { hosts: [ '127.0.0.1:9002', 'http://127.0.0.1:9003' ] }, | ||
62 | token: server.accessToken, | ||
63 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
64 | }) | ||
65 | }) | ||
66 | |||
67 | it('Should fail if hosts are not unique', async function () { | ||
68 | await makePostBodyRequest({ | ||
69 | url: server.url, | ||
70 | path, | ||
71 | fields: { urls: [ '127.0.0.1:9002', '127.0.0.1:9002' ] }, | ||
72 | token: server.accessToken, | ||
73 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
74 | }) | ||
75 | }) | ||
76 | |||
77 | it('Should fail if handles is not composed by handles', async function () { | ||
78 | await makePostBodyRequest({ | ||
79 | url: server.url, | ||
80 | path, | ||
81 | fields: { handles: [ 'hello@example.com', '127.0.0.1:9001' ] }, | ||
82 | token: server.accessToken, | ||
83 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
84 | }) | ||
85 | }) | ||
86 | |||
87 | it('Should fail if handles are not unique', async function () { | ||
88 | await makePostBodyRequest({ | ||
89 | url: server.url, | ||
90 | path, | ||
91 | fields: { urls: [ 'hello@example.com', 'hello@example.com' ] }, | ||
92 | token: server.accessToken, | ||
93 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
94 | }) | ||
95 | }) | ||
96 | |||
97 | it('Should fail with an invalid token', async function () { | ||
98 | await makePostBodyRequest({ | ||
99 | url: server.url, | ||
100 | path, | ||
101 | fields: { hosts: [ '127.0.0.1:9002' ] }, | ||
102 | token: 'fake_token', | ||
103 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
104 | }) | ||
105 | }) | ||
106 | |||
107 | it('Should fail if the user is not an administrator', async function () { | ||
108 | await makePostBodyRequest({ | ||
109 | url: server.url, | ||
110 | path, | ||
111 | fields: { hosts: [ '127.0.0.1:9002' ] }, | ||
112 | token: userAccessToken, | ||
113 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
114 | }) | ||
115 | }) | ||
116 | }) | ||
117 | |||
118 | describe('When listing followings', function () { | ||
119 | const path = '/api/v1/server/following' | ||
120 | |||
121 | it('Should fail with a bad start pagination', async function () { | ||
122 | await checkBadStartPagination(server.url, path) | ||
123 | }) | ||
124 | |||
125 | it('Should fail with a bad count pagination', async function () { | ||
126 | await checkBadCountPagination(server.url, path) | ||
127 | }) | ||
128 | |||
129 | it('Should fail with an incorrect sort', async function () { | ||
130 | await checkBadSortPagination(server.url, path) | ||
131 | }) | ||
132 | |||
133 | it('Should fail with an incorrect state', async function () { | ||
134 | await makeGetRequest({ | ||
135 | url: server.url, | ||
136 | path, | ||
137 | query: { | ||
138 | state: 'blabla' | ||
139 | } | ||
140 | }) | ||
141 | }) | ||
142 | |||
143 | it('Should fail with an incorrect actor type', async function () { | ||
144 | await makeGetRequest({ | ||
145 | url: server.url, | ||
146 | path, | ||
147 | query: { | ||
148 | actorType: 'blabla' | ||
149 | } | ||
150 | }) | ||
151 | }) | ||
152 | |||
153 | it('Should fail succeed with the correct params', async function () { | ||
154 | await makeGetRequest({ | ||
155 | url: server.url, | ||
156 | path, | ||
157 | expectedStatus: HttpStatusCode.OK_200, | ||
158 | query: { | ||
159 | state: 'accepted', | ||
160 | actorType: 'Application' | ||
161 | } | ||
162 | }) | ||
163 | }) | ||
164 | }) | ||
165 | |||
166 | describe('When listing followers', function () { | ||
167 | const path = '/api/v1/server/followers' | ||
168 | |||
169 | it('Should fail with a bad start pagination', async function () { | ||
170 | await checkBadStartPagination(server.url, path) | ||
171 | }) | ||
172 | |||
173 | it('Should fail with a bad count pagination', async function () { | ||
174 | await checkBadCountPagination(server.url, path) | ||
175 | }) | ||
176 | |||
177 | it('Should fail with an incorrect sort', async function () { | ||
178 | await checkBadSortPagination(server.url, path) | ||
179 | }) | ||
180 | |||
181 | it('Should fail with an incorrect actor type', async function () { | ||
182 | await makeGetRequest({ | ||
183 | url: server.url, | ||
184 | path, | ||
185 | query: { | ||
186 | actorType: 'blabla' | ||
187 | } | ||
188 | }) | ||
189 | }) | ||
190 | |||
191 | it('Should fail with an incorrect state', async function () { | ||
192 | await makeGetRequest({ | ||
193 | url: server.url, | ||
194 | path, | ||
195 | query: { | ||
196 | state: 'blabla', | ||
197 | actorType: 'Application' | ||
198 | } | ||
199 | }) | ||
200 | }) | ||
201 | |||
202 | it('Should fail succeed with the correct params', async function () { | ||
203 | await makeGetRequest({ | ||
204 | url: server.url, | ||
205 | path, | ||
206 | expectedStatus: HttpStatusCode.OK_200, | ||
207 | query: { | ||
208 | state: 'accepted' | ||
209 | } | ||
210 | }) | ||
211 | }) | ||
212 | }) | ||
213 | |||
214 | describe('When removing a follower', function () { | ||
215 | const path = '/api/v1/server/followers' | ||
216 | |||
217 | it('Should fail with an invalid token', async function () { | ||
218 | await makeDeleteRequest({ | ||
219 | url: server.url, | ||
220 | path: path + '/toto@127.0.0.1:9002', | ||
221 | token: 'fake_token', | ||
222 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
223 | }) | ||
224 | }) | ||
225 | |||
226 | it('Should fail if the user is not an administrator', async function () { | ||
227 | await makeDeleteRequest({ | ||
228 | url: server.url, | ||
229 | path: path + '/toto@127.0.0.1:9002', | ||
230 | token: userAccessToken, | ||
231 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
232 | }) | ||
233 | }) | ||
234 | |||
235 | it('Should fail with an invalid follower', async function () { | ||
236 | await makeDeleteRequest({ | ||
237 | url: server.url, | ||
238 | path: path + '/toto', | ||
239 | token: server.accessToken, | ||
240 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
241 | }) | ||
242 | }) | ||
243 | |||
244 | it('Should fail with an unknown follower', async function () { | ||
245 | await makeDeleteRequest({ | ||
246 | url: server.url, | ||
247 | path: path + '/toto@127.0.0.1:9003', | ||
248 | token: server.accessToken, | ||
249 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
250 | }) | ||
251 | }) | ||
252 | }) | ||
253 | |||
254 | describe('When accepting a follower', function () { | ||
255 | const path = '/api/v1/server/followers' | ||
256 | |||
257 | it('Should fail with an invalid token', async function () { | ||
258 | await makePostBodyRequest({ | ||
259 | url: server.url, | ||
260 | path: path + '/toto@127.0.0.1:9002/accept', | ||
261 | token: 'fake_token', | ||
262 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
263 | }) | ||
264 | }) | ||
265 | |||
266 | it('Should fail if the user is not an administrator', async function () { | ||
267 | await makePostBodyRequest({ | ||
268 | url: server.url, | ||
269 | path: path + '/toto@127.0.0.1:9002/accept', | ||
270 | token: userAccessToken, | ||
271 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
272 | }) | ||
273 | }) | ||
274 | |||
275 | it('Should fail with an invalid follower', async function () { | ||
276 | await makePostBodyRequest({ | ||
277 | url: server.url, | ||
278 | path: path + '/toto/accept', | ||
279 | token: server.accessToken, | ||
280 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
281 | }) | ||
282 | }) | ||
283 | |||
284 | it('Should fail with an unknown follower', async function () { | ||
285 | await makePostBodyRequest({ | ||
286 | url: server.url, | ||
287 | path: path + '/toto@127.0.0.1:9003/accept', | ||
288 | token: server.accessToken, | ||
289 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
290 | }) | ||
291 | }) | ||
292 | }) | ||
293 | |||
294 | describe('When rejecting a follower', function () { | ||
295 | const path = '/api/v1/server/followers' | ||
296 | |||
297 | it('Should fail with an invalid token', async function () { | ||
298 | await makePostBodyRequest({ | ||
299 | url: server.url, | ||
300 | path: path + '/toto@127.0.0.1:9002/reject', | ||
301 | token: 'fake_token', | ||
302 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
303 | }) | ||
304 | }) | ||
305 | |||
306 | it('Should fail if the user is not an administrator', async function () { | ||
307 | await makePostBodyRequest({ | ||
308 | url: server.url, | ||
309 | path: path + '/toto@127.0.0.1:9002/reject', | ||
310 | token: userAccessToken, | ||
311 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
312 | }) | ||
313 | }) | ||
314 | |||
315 | it('Should fail with an invalid follower', async function () { | ||
316 | await makePostBodyRequest({ | ||
317 | url: server.url, | ||
318 | path: path + '/toto/reject', | ||
319 | token: server.accessToken, | ||
320 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
321 | }) | ||
322 | }) | ||
323 | |||
324 | it('Should fail with an unknown follower', async function () { | ||
325 | await makePostBodyRequest({ | ||
326 | url: server.url, | ||
327 | path: path + '/toto@127.0.0.1:9003/reject', | ||
328 | token: server.accessToken, | ||
329 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
330 | }) | ||
331 | }) | ||
332 | }) | ||
333 | |||
334 | describe('When removing following', function () { | ||
335 | const path = '/api/v1/server/following' | ||
336 | |||
337 | it('Should fail with an invalid token', async function () { | ||
338 | await makeDeleteRequest({ | ||
339 | url: server.url, | ||
340 | path: path + '/127.0.0.1:9002', | ||
341 | token: 'fake_token', | ||
342 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
343 | }) | ||
344 | }) | ||
345 | |||
346 | it('Should fail if the user is not an administrator', async function () { | ||
347 | await makeDeleteRequest({ | ||
348 | url: server.url, | ||
349 | path: path + '/127.0.0.1:9002', | ||
350 | token: userAccessToken, | ||
351 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
352 | }) | ||
353 | }) | ||
354 | |||
355 | it('Should fail if we do not follow this server', async function () { | ||
356 | await makeDeleteRequest({ | ||
357 | url: server.url, | ||
358 | path: path + '/example.com', | ||
359 | token: server.accessToken, | ||
360 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
361 | }) | ||
362 | }) | ||
363 | }) | ||
364 | }) | ||
365 | |||
366 | after(async function () { | ||
367 | await cleanupTests([ server ]) | ||
368 | }) | ||
369 | }) | ||
diff --git a/packages/tests/src/api/check-params/index.ts b/packages/tests/src/api/check-params/index.ts new file mode 100644 index 000000000..ed5fe6b06 --- /dev/null +++ b/packages/tests/src/api/check-params/index.ts | |||
@@ -0,0 +1,45 @@ | |||
1 | import './abuses.js' | ||
2 | import './accounts.js' | ||
3 | import './blocklist.js' | ||
4 | import './bulk.js' | ||
5 | import './channel-import-videos.js' | ||
6 | import './config.js' | ||
7 | import './contact-form.js' | ||
8 | import './custom-pages.js' | ||
9 | import './debug.js' | ||
10 | import './follows.js' | ||
11 | import './jobs.js' | ||
12 | import './live.js' | ||
13 | import './logs.js' | ||
14 | import './metrics.js' | ||
15 | import './my-user.js' | ||
16 | import './plugins.js' | ||
17 | import './redundancy.js' | ||
18 | import './registrations.js' | ||
19 | import './runners.js' | ||
20 | import './search.js' | ||
21 | import './services.js' | ||
22 | import './transcoding.js' | ||
23 | import './two-factor.js' | ||
24 | import './upload-quota.js' | ||
25 | import './user-notifications.js' | ||
26 | import './user-subscriptions.js' | ||
27 | import './users-admin.js' | ||
28 | import './users-emails.js' | ||
29 | import './video-blacklist.js' | ||
30 | import './video-captions.js' | ||
31 | import './video-channel-syncs.js' | ||
32 | import './video-channels.js' | ||
33 | import './video-comments.js' | ||
34 | import './video-files.js' | ||
35 | import './video-imports.js' | ||
36 | import './video-playlists.js' | ||
37 | import './video-storyboards.js' | ||
38 | import './video-source.js' | ||
39 | import './video-studio.js' | ||
40 | import './video-token.js' | ||
41 | import './videos-common-filters.js' | ||
42 | import './videos-history.js' | ||
43 | import './videos-overviews.js' | ||
44 | import './videos.js' | ||
45 | import './views.js' | ||
diff --git a/packages/tests/src/api/check-params/jobs.ts b/packages/tests/src/api/check-params/jobs.ts new file mode 100644 index 000000000..331d58c6a --- /dev/null +++ b/packages/tests/src/api/check-params/jobs.ts | |||
@@ -0,0 +1,125 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeGetRequest, | ||
9 | makePostBodyRequest, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test jobs API validators', function () { | ||
15 | const path = '/api/v1/jobs/failed' | ||
16 | let server: PeerTubeServer | ||
17 | let userAccessToken = '' | ||
18 | |||
19 | // --------------------------------------------------------------- | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(120000) | ||
23 | |||
24 | server = await createSingleServer(1) | ||
25 | |||
26 | await setAccessTokensToServers([ server ]) | ||
27 | |||
28 | const user = { | ||
29 | username: 'user1', | ||
30 | password: 'my super password' | ||
31 | } | ||
32 | await server.users.create({ username: user.username, password: user.password }) | ||
33 | userAccessToken = await server.login.getAccessToken(user) | ||
34 | }) | ||
35 | |||
36 | describe('When listing jobs', function () { | ||
37 | |||
38 | it('Should fail with a bad state', async function () { | ||
39 | await makeGetRequest({ | ||
40 | url: server.url, | ||
41 | token: server.accessToken, | ||
42 | path: path + 'ade' | ||
43 | }) | ||
44 | }) | ||
45 | |||
46 | it('Should fail with an incorrect job type', async function () { | ||
47 | await makeGetRequest({ | ||
48 | url: server.url, | ||
49 | token: server.accessToken, | ||
50 | path, | ||
51 | query: { | ||
52 | jobType: 'toto' | ||
53 | } | ||
54 | }) | ||
55 | }) | ||
56 | |||
57 | it('Should fail with a bad start pagination', async function () { | ||
58 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
59 | }) | ||
60 | |||
61 | it('Should fail with a bad count pagination', async function () { | ||
62 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
63 | }) | ||
64 | |||
65 | it('Should fail with an incorrect sort', async function () { | ||
66 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
67 | }) | ||
68 | |||
69 | it('Should fail with a non authenticated user', async function () { | ||
70 | await makeGetRequest({ | ||
71 | url: server.url, | ||
72 | path, | ||
73 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
74 | }) | ||
75 | }) | ||
76 | |||
77 | it('Should fail with a non admin user', async function () { | ||
78 | await makeGetRequest({ | ||
79 | url: server.url, | ||
80 | path, | ||
81 | token: userAccessToken, | ||
82 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
83 | }) | ||
84 | }) | ||
85 | }) | ||
86 | |||
87 | describe('When pausing/resuming the job queue', async function () { | ||
88 | const commands = [ 'pause', 'resume' ] | ||
89 | |||
90 | it('Should fail with a non authenticated user', async function () { | ||
91 | for (const command of commands) { | ||
92 | await makePostBodyRequest({ | ||
93 | url: server.url, | ||
94 | path: '/api/v1/jobs/' + command, | ||
95 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
96 | }) | ||
97 | } | ||
98 | }) | ||
99 | |||
100 | it('Should fail with a non admin user', async function () { | ||
101 | for (const command of commands) { | ||
102 | await makePostBodyRequest({ | ||
103 | url: server.url, | ||
104 | path: '/api/v1/jobs/' + command, | ||
105 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
106 | }) | ||
107 | } | ||
108 | }) | ||
109 | |||
110 | it('Should succeed with the correct params', async function () { | ||
111 | for (const command of commands) { | ||
112 | await makePostBodyRequest({ | ||
113 | url: server.url, | ||
114 | path: '/api/v1/jobs/' + command, | ||
115 | token: server.accessToken, | ||
116 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
117 | }) | ||
118 | } | ||
119 | }) | ||
120 | }) | ||
121 | |||
122 | after(async function () { | ||
123 | await cleanupTests([ server ]) | ||
124 | }) | ||
125 | }) | ||
diff --git a/packages/tests/src/api/check-params/live.ts b/packages/tests/src/api/check-params/live.ts new file mode 100644 index 000000000..5900823ea --- /dev/null +++ b/packages/tests/src/api/check-params/live.ts | |||
@@ -0,0 +1,590 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { omit } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | LiveCommand, | ||
11 | makePostBodyRequest, | ||
12 | makeUploadRequest, | ||
13 | PeerTubeServer, | ||
14 | sendRTMPStream, | ||
15 | setAccessTokensToServers, | ||
16 | stopFfmpeg | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | |||
19 | describe('Test video lives API validator', function () { | ||
20 | const path = '/api/v1/videos/live' | ||
21 | let server: PeerTubeServer | ||
22 | let userAccessToken = '' | ||
23 | let channelId: number | ||
24 | let video: VideoCreateResult | ||
25 | let videoIdNotLive: number | ||
26 | let command: LiveCommand | ||
27 | |||
28 | // --------------------------------------------------------------- | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(30000) | ||
32 | |||
33 | server = await createSingleServer(1) | ||
34 | |||
35 | await setAccessTokensToServers([ server ]) | ||
36 | |||
37 | await server.config.updateCustomSubConfig({ | ||
38 | newConfig: { | ||
39 | live: { | ||
40 | enabled: true, | ||
41 | latencySetting: { | ||
42 | enabled: false | ||
43 | }, | ||
44 | maxInstanceLives: 20, | ||
45 | maxUserLives: 20, | ||
46 | allowReplay: true | ||
47 | } | ||
48 | } | ||
49 | }) | ||
50 | |||
51 | const username = 'user1' | ||
52 | const password = 'my super password' | ||
53 | await server.users.create({ username, password }) | ||
54 | userAccessToken = await server.login.getAccessToken({ username, password }) | ||
55 | |||
56 | { | ||
57 | const { videoChannels } = await server.users.getMyInfo() | ||
58 | channelId = videoChannels[0].id | ||
59 | } | ||
60 | |||
61 | { | ||
62 | videoIdNotLive = (await server.videos.quickUpload({ name: 'not live' })).id | ||
63 | } | ||
64 | |||
65 | command = server.live | ||
66 | }) | ||
67 | |||
68 | describe('When creating a live', function () { | ||
69 | let baseCorrectParams | ||
70 | |||
71 | before(function () { | ||
72 | baseCorrectParams = { | ||
73 | name: 'my super name', | ||
74 | category: 5, | ||
75 | licence: 1, | ||
76 | language: 'pt', | ||
77 | nsfw: false, | ||
78 | commentsEnabled: true, | ||
79 | downloadEnabled: true, | ||
80 | waitTranscoding: true, | ||
81 | description: 'my super description', | ||
82 | support: 'my super support text', | ||
83 | tags: [ 'tag1', 'tag2' ], | ||
84 | privacy: VideoPrivacy.PUBLIC, | ||
85 | channelId, | ||
86 | saveReplay: false, | ||
87 | replaySettings: undefined, | ||
88 | permanentLive: false, | ||
89 | latencyMode: LiveVideoLatencyMode.DEFAULT | ||
90 | } | ||
91 | }) | ||
92 | |||
93 | it('Should fail with nothing', async function () { | ||
94 | const fields = {} | ||
95 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
96 | }) | ||
97 | |||
98 | it('Should fail with a long name', async function () { | ||
99 | const fields = { ...baseCorrectParams, name: 'super'.repeat(65) } | ||
100 | |||
101 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
102 | }) | ||
103 | |||
104 | it('Should fail with a bad category', async function () { | ||
105 | const fields = { ...baseCorrectParams, category: 125 } | ||
106 | |||
107 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
108 | }) | ||
109 | |||
110 | it('Should fail with a bad licence', async function () { | ||
111 | const fields = { ...baseCorrectParams, licence: 125 } | ||
112 | |||
113 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
114 | }) | ||
115 | |||
116 | it('Should fail with a bad language', async function () { | ||
117 | const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } | ||
118 | |||
119 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
120 | }) | ||
121 | |||
122 | it('Should fail with a long description', async function () { | ||
123 | const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) } | ||
124 | |||
125 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
126 | }) | ||
127 | |||
128 | it('Should fail with a long support text', async function () { | ||
129 | const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } | ||
130 | |||
131 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
132 | }) | ||
133 | |||
134 | it('Should fail without a channel', async function () { | ||
135 | const fields = omit(baseCorrectParams, [ 'channelId' ]) | ||
136 | |||
137 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
138 | }) | ||
139 | |||
140 | it('Should fail with a bad channel', async function () { | ||
141 | const fields = { ...baseCorrectParams, channelId: 545454 } | ||
142 | |||
143 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
144 | }) | ||
145 | |||
146 | it('Should fail with a bad privacy for replay settings', async function () { | ||
147 | const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: 999 } } | ||
148 | |||
149 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
150 | }) | ||
151 | |||
152 | it('Should fail with another user channel', async function () { | ||
153 | const user = { | ||
154 | username: 'fake', | ||
155 | password: 'fake_password' | ||
156 | } | ||
157 | await server.users.create({ username: user.username, password: user.password }) | ||
158 | |||
159 | const accessTokenUser = await server.login.getAccessToken(user) | ||
160 | const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser }) | ||
161 | const customChannelId = videoChannels[0].id | ||
162 | |||
163 | const fields = { ...baseCorrectParams, channelId: customChannelId } | ||
164 | |||
165 | await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields }) | ||
166 | }) | ||
167 | |||
168 | it('Should fail with too many tags', async function () { | ||
169 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] } | ||
170 | |||
171 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
172 | }) | ||
173 | |||
174 | it('Should fail with a tag length too low', async function () { | ||
175 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] } | ||
176 | |||
177 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
178 | }) | ||
179 | |||
180 | it('Should fail with a tag length too big', async function () { | ||
181 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] } | ||
182 | |||
183 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
184 | }) | ||
185 | |||
186 | it('Should fail with an incorrect thumbnail file', async function () { | ||
187 | const fields = baseCorrectParams | ||
188 | const attaches = { | ||
189 | thumbnailfile: buildAbsoluteFixturePath('video_short.mp4') | ||
190 | } | ||
191 | |||
192 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
193 | }) | ||
194 | |||
195 | it('Should fail with a big thumbnail file', async function () { | ||
196 | const fields = baseCorrectParams | ||
197 | const attaches = { | ||
198 | thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') | ||
199 | } | ||
200 | |||
201 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
202 | }) | ||
203 | |||
204 | it('Should fail with an incorrect preview file', async function () { | ||
205 | const fields = baseCorrectParams | ||
206 | const attaches = { | ||
207 | previewfile: buildAbsoluteFixturePath('video_short.mp4') | ||
208 | } | ||
209 | |||
210 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
211 | }) | ||
212 | |||
213 | it('Should fail with a big preview file', async function () { | ||
214 | const fields = baseCorrectParams | ||
215 | const attaches = { | ||
216 | previewfile: buildAbsoluteFixturePath('custom-preview-big.png') | ||
217 | } | ||
218 | |||
219 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
220 | }) | ||
221 | |||
222 | it('Should fail with bad latency setting', async function () { | ||
223 | const fields = { ...baseCorrectParams, latencyMode: 42 } | ||
224 | |||
225 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
226 | }) | ||
227 | |||
228 | it('Should fail to set latency if the server does not allow it', async function () { | ||
229 | const fields = { ...baseCorrectParams, latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } | ||
230 | |||
231 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
232 | }) | ||
233 | |||
234 | it('Should succeed with the correct parameters', async function () { | ||
235 | this.timeout(30000) | ||
236 | |||
237 | const res = await makePostBodyRequest({ | ||
238 | url: server.url, | ||
239 | path, | ||
240 | token: server.accessToken, | ||
241 | fields: baseCorrectParams, | ||
242 | expectedStatus: HttpStatusCode.OK_200 | ||
243 | }) | ||
244 | |||
245 | video = res.body.video | ||
246 | }) | ||
247 | |||
248 | it('Should forbid if live is disabled', async function () { | ||
249 | await server.config.updateCustomSubConfig({ | ||
250 | newConfig: { | ||
251 | live: { | ||
252 | enabled: false | ||
253 | } | ||
254 | } | ||
255 | }) | ||
256 | |||
257 | await makePostBodyRequest({ | ||
258 | url: server.url, | ||
259 | path, | ||
260 | token: server.accessToken, | ||
261 | fields: baseCorrectParams, | ||
262 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
263 | }) | ||
264 | }) | ||
265 | |||
266 | it('Should forbid to save replay if not enabled by the admin', async function () { | ||
267 | const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } | ||
268 | |||
269 | await server.config.updateCustomSubConfig({ | ||
270 | newConfig: { | ||
271 | live: { | ||
272 | enabled: true, | ||
273 | allowReplay: false | ||
274 | } | ||
275 | } | ||
276 | }) | ||
277 | |||
278 | await makePostBodyRequest({ | ||
279 | url: server.url, | ||
280 | path, | ||
281 | token: server.accessToken, | ||
282 | fields, | ||
283 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
284 | }) | ||
285 | }) | ||
286 | |||
287 | it('Should allow to save replay if enabled by the admin', async function () { | ||
288 | const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } | ||
289 | |||
290 | await server.config.updateCustomSubConfig({ | ||
291 | newConfig: { | ||
292 | live: { | ||
293 | enabled: true, | ||
294 | allowReplay: true | ||
295 | } | ||
296 | } | ||
297 | }) | ||
298 | |||
299 | await makePostBodyRequest({ | ||
300 | url: server.url, | ||
301 | path, | ||
302 | token: server.accessToken, | ||
303 | fields, | ||
304 | expectedStatus: HttpStatusCode.OK_200 | ||
305 | }) | ||
306 | }) | ||
307 | |||
308 | it('Should not allow live if max instance lives is reached', async function () { | ||
309 | await server.config.updateCustomSubConfig({ | ||
310 | newConfig: { | ||
311 | live: { | ||
312 | enabled: true, | ||
313 | maxInstanceLives: 1 | ||
314 | } | ||
315 | } | ||
316 | }) | ||
317 | |||
318 | await makePostBodyRequest({ | ||
319 | url: server.url, | ||
320 | path, | ||
321 | token: server.accessToken, | ||
322 | fields: baseCorrectParams, | ||
323 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
324 | }) | ||
325 | }) | ||
326 | |||
327 | it('Should not allow live if max user lives is reached', async function () { | ||
328 | await server.config.updateCustomSubConfig({ | ||
329 | newConfig: { | ||
330 | live: { | ||
331 | enabled: true, | ||
332 | maxInstanceLives: 20, | ||
333 | maxUserLives: 1 | ||
334 | } | ||
335 | } | ||
336 | }) | ||
337 | |||
338 | await makePostBodyRequest({ | ||
339 | url: server.url, | ||
340 | path, | ||
341 | token: server.accessToken, | ||
342 | fields: baseCorrectParams, | ||
343 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
344 | }) | ||
345 | }) | ||
346 | }) | ||
347 | |||
348 | describe('When getting live information', function () { | ||
349 | |||
350 | it('Should fail with a bad access token', async function () { | ||
351 | await command.get({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
352 | }) | ||
353 | |||
354 | it('Should not display private information without access token', async function () { | ||
355 | const live = await command.get({ token: '', videoId: video.id }) | ||
356 | |||
357 | expect(live.rtmpUrl).to.not.exist | ||
358 | expect(live.streamKey).to.not.exist | ||
359 | expect(live.latencyMode).to.exist | ||
360 | }) | ||
361 | |||
362 | it('Should not display private information with token of another user', async function () { | ||
363 | const live = await command.get({ token: userAccessToken, videoId: video.id }) | ||
364 | |||
365 | expect(live.rtmpUrl).to.not.exist | ||
366 | expect(live.streamKey).to.not.exist | ||
367 | expect(live.latencyMode).to.exist | ||
368 | }) | ||
369 | |||
370 | it('Should display private information with appropriate token', async function () { | ||
371 | const live = await command.get({ videoId: video.id }) | ||
372 | |||
373 | expect(live.rtmpUrl).to.exist | ||
374 | expect(live.streamKey).to.exist | ||
375 | expect(live.latencyMode).to.exist | ||
376 | }) | ||
377 | |||
378 | it('Should fail with a bad video id', async function () { | ||
379 | await command.get({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
380 | }) | ||
381 | |||
382 | it('Should fail with an unknown video id', async function () { | ||
383 | await command.get({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
384 | }) | ||
385 | |||
386 | it('Should fail with a non live video', async function () { | ||
387 | await command.get({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
388 | }) | ||
389 | |||
390 | it('Should succeed with the correct params', async function () { | ||
391 | await command.get({ videoId: video.id }) | ||
392 | await command.get({ videoId: video.uuid }) | ||
393 | await command.get({ videoId: video.shortUUID }) | ||
394 | }) | ||
395 | }) | ||
396 | |||
397 | describe('When getting live sessions', function () { | ||
398 | |||
399 | it('Should fail with a bad access token', async function () { | ||
400 | await command.listSessions({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
401 | }) | ||
402 | |||
403 | it('Should fail without token', async function () { | ||
404 | await command.listSessions({ token: null, videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
405 | }) | ||
406 | |||
407 | it('Should fail with the token of another user', async function () { | ||
408 | await command.listSessions({ token: userAccessToken, videoId: video.id, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
409 | }) | ||
410 | |||
411 | it('Should fail with a bad video id', async function () { | ||
412 | await command.listSessions({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
413 | }) | ||
414 | |||
415 | it('Should fail with an unknown video id', async function () { | ||
416 | await command.listSessions({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
417 | }) | ||
418 | |||
419 | it('Should fail with a non live video', async function () { | ||
420 | await command.listSessions({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
421 | }) | ||
422 | |||
423 | it('Should succeed with the correct params', async function () { | ||
424 | await command.listSessions({ videoId: video.id }) | ||
425 | }) | ||
426 | }) | ||
427 | |||
428 | describe('When getting live session of a replay', function () { | ||
429 | |||
430 | it('Should fail with a bad video id', async function () { | ||
431 | await command.getReplaySession({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
432 | }) | ||
433 | |||
434 | it('Should fail with an unknown video id', async function () { | ||
435 | await command.getReplaySession({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
436 | }) | ||
437 | |||
438 | it('Should fail with a non replay video', async function () { | ||
439 | await command.getReplaySession({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
440 | }) | ||
441 | }) | ||
442 | |||
443 | describe('When updating live information', async function () { | ||
444 | |||
445 | it('Should fail without access token', async function () { | ||
446 | await command.update({ token: '', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
447 | }) | ||
448 | |||
449 | it('Should fail with a bad access token', async function () { | ||
450 | await command.update({ token: 'toto', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
451 | }) | ||
452 | |||
453 | it('Should fail with access token of another user', async function () { | ||
454 | await command.update({ token: userAccessToken, videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
455 | }) | ||
456 | |||
457 | it('Should fail with a bad video id', async function () { | ||
458 | await command.update({ videoId: 'toto', fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
459 | }) | ||
460 | |||
461 | it('Should fail with an unknown video id', async function () { | ||
462 | await command.update({ videoId: 454555, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
463 | }) | ||
464 | |||
465 | it('Should fail with a non live video', async function () { | ||
466 | await command.update({ videoId: videoIdNotLive, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
467 | }) | ||
468 | |||
469 | it('Should fail with bad latency setting', async function () { | ||
470 | const fields = { latencyMode: 42 as any } | ||
471 | |||
472 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
473 | }) | ||
474 | |||
475 | it('Should fail with a bad privacy for replay settings', async function () { | ||
476 | const fields = { saveReplay: true, replaySettings: { privacy: 999 as any } } | ||
477 | |||
478 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
479 | }) | ||
480 | |||
481 | it('Should fail with save replay enabled but without replay settings', async function () { | ||
482 | await server.config.updateCustomSubConfig({ | ||
483 | newConfig: { | ||
484 | live: { | ||
485 | enabled: true, | ||
486 | allowReplay: true | ||
487 | } | ||
488 | } | ||
489 | }) | ||
490 | |||
491 | const fields = { saveReplay: true } | ||
492 | |||
493 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
494 | }) | ||
495 | |||
496 | it('Should fail with save replay disabled and replay settings', async function () { | ||
497 | const fields = { saveReplay: false, replaySettings: { privacy: VideoPrivacy.INTERNAL } } | ||
498 | |||
499 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
500 | }) | ||
501 | |||
502 | it('Should fail with only replay settings when save replay is disabled', async function () { | ||
503 | const fields = { replaySettings: { privacy: VideoPrivacy.INTERNAL } } | ||
504 | |||
505 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
506 | }) | ||
507 | |||
508 | it('Should fail to set latency if the server does not allow it', async function () { | ||
509 | const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } | ||
510 | |||
511 | await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
512 | }) | ||
513 | |||
514 | it('Should succeed with the correct params', async function () { | ||
515 | await command.update({ videoId: video.id, fields: { saveReplay: false } }) | ||
516 | await command.update({ videoId: video.uuid, fields: { saveReplay: false } }) | ||
517 | await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } }) | ||
518 | |||
519 | await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } }) | ||
520 | |||
521 | }) | ||
522 | |||
523 | it('Should fail to update replay status if replay is not allowed on the instance', async function () { | ||
524 | await server.config.updateCustomSubConfig({ | ||
525 | newConfig: { | ||
526 | live: { | ||
527 | enabled: true, | ||
528 | allowReplay: false | ||
529 | } | ||
530 | } | ||
531 | }) | ||
532 | |||
533 | await command.update({ videoId: video.id, fields: { saveReplay: true }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
534 | }) | ||
535 | |||
536 | it('Should fail to update a live if it has already started', async function () { | ||
537 | this.timeout(40000) | ||
538 | |||
539 | const live = await command.get({ videoId: video.id }) | ||
540 | |||
541 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
542 | |||
543 | await command.waitUntilPublished({ videoId: video.id }) | ||
544 | await command.update({ videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
545 | |||
546 | await stopFfmpeg(ffmpegCommand) | ||
547 | }) | ||
548 | |||
549 | it('Should fail to change live privacy if it has already started', async function () { | ||
550 | this.timeout(40000) | ||
551 | |||
552 | const live = await command.get({ videoId: video.id }) | ||
553 | |||
554 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
555 | |||
556 | await command.waitUntilPublished({ videoId: video.id }) | ||
557 | |||
558 | await server.videos.update({ | ||
559 | id: video.id, | ||
560 | attributes: { privacy: VideoPrivacy.PUBLIC } // Same privacy, it's fine | ||
561 | }) | ||
562 | |||
563 | await server.videos.update({ | ||
564 | id: video.id, | ||
565 | attributes: { privacy: VideoPrivacy.UNLISTED }, | ||
566 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
567 | }) | ||
568 | |||
569 | await stopFfmpeg(ffmpegCommand) | ||
570 | }) | ||
571 | |||
572 | it('Should fail to stream twice in the save live', async function () { | ||
573 | this.timeout(40000) | ||
574 | |||
575 | const live = await command.get({ videoId: video.id }) | ||
576 | |||
577 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
578 | |||
579 | await command.waitUntilPublished({ videoId: video.id }) | ||
580 | |||
581 | await command.runAndTestStreamError({ videoId: video.id, shouldHaveError: true }) | ||
582 | |||
583 | await stopFfmpeg(ffmpegCommand) | ||
584 | }) | ||
585 | }) | ||
586 | |||
587 | after(async function () { | ||
588 | await cleanupTests([ server ]) | ||
589 | }) | ||
590 | }) | ||
diff --git a/packages/tests/src/api/check-params/logs.ts b/packages/tests/src/api/check-params/logs.ts new file mode 100644 index 000000000..629530e30 --- /dev/null +++ b/packages/tests/src/api/check-params/logs.ts | |||
@@ -0,0 +1,163 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeGetRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test logs API validators', function () { | ||
14 | const path = '/api/v1/server/logs' | ||
15 | let server: PeerTubeServer | ||
16 | let userAccessToken = '' | ||
17 | |||
18 | // --------------------------------------------------------------- | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(120000) | ||
22 | |||
23 | server = await createSingleServer(1) | ||
24 | |||
25 | await setAccessTokensToServers([ server ]) | ||
26 | |||
27 | const user = { | ||
28 | username: 'user1', | ||
29 | password: 'my super password' | ||
30 | } | ||
31 | await server.users.create({ username: user.username, password: user.password }) | ||
32 | userAccessToken = await server.login.getAccessToken(user) | ||
33 | }) | ||
34 | |||
35 | describe('When getting logs', function () { | ||
36 | |||
37 | it('Should fail with a non authenticated user', async function () { | ||
38 | await makeGetRequest({ | ||
39 | url: server.url, | ||
40 | path, | ||
41 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
42 | }) | ||
43 | }) | ||
44 | |||
45 | it('Should fail with a non admin user', async function () { | ||
46 | await makeGetRequest({ | ||
47 | url: server.url, | ||
48 | path, | ||
49 | token: userAccessToken, | ||
50 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
51 | }) | ||
52 | }) | ||
53 | |||
54 | it('Should fail with a missing startDate query', async function () { | ||
55 | await makeGetRequest({ | ||
56 | url: server.url, | ||
57 | path, | ||
58 | token: server.accessToken, | ||
59 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
60 | }) | ||
61 | }) | ||
62 | |||
63 | it('Should fail with a bad startDate query', async function () { | ||
64 | await makeGetRequest({ | ||
65 | url: server.url, | ||
66 | path, | ||
67 | token: server.accessToken, | ||
68 | query: { startDate: 'toto' }, | ||
69 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
70 | }) | ||
71 | }) | ||
72 | |||
73 | it('Should fail with a bad endDate query', async function () { | ||
74 | await makeGetRequest({ | ||
75 | url: server.url, | ||
76 | path, | ||
77 | token: server.accessToken, | ||
78 | query: { startDate: new Date().toISOString(), endDate: 'toto' }, | ||
79 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
80 | }) | ||
81 | }) | ||
82 | |||
83 | it('Should fail with a bad level parameter', async function () { | ||
84 | await makeGetRequest({ | ||
85 | url: server.url, | ||
86 | path, | ||
87 | token: server.accessToken, | ||
88 | query: { startDate: new Date().toISOString(), level: 'toto' }, | ||
89 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
90 | }) | ||
91 | }) | ||
92 | |||
93 | it('Should succeed with the correct params', async function () { | ||
94 | await makeGetRequest({ | ||
95 | url: server.url, | ||
96 | path, | ||
97 | token: server.accessToken, | ||
98 | query: { startDate: new Date().toISOString() }, | ||
99 | expectedStatus: HttpStatusCode.OK_200 | ||
100 | }) | ||
101 | }) | ||
102 | }) | ||
103 | |||
104 | describe('When creating client logs', function () { | ||
105 | const base = { | ||
106 | level: 'warn' as 'warn', | ||
107 | message: 'my super message', | ||
108 | url: 'https://example.com/toto' | ||
109 | } | ||
110 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
111 | |||
112 | it('Should fail with an invalid level', async function () { | ||
113 | await server.logs.createLogClient({ payload: { ...base, level: '' as any }, expectedStatus }) | ||
114 | await server.logs.createLogClient({ payload: { ...base, level: undefined }, expectedStatus }) | ||
115 | await server.logs.createLogClient({ payload: { ...base, level: 'toto' as any }, expectedStatus }) | ||
116 | }) | ||
117 | |||
118 | it('Should fail with an invalid message', async function () { | ||
119 | await server.logs.createLogClient({ payload: { ...base, message: undefined }, expectedStatus }) | ||
120 | await server.logs.createLogClient({ payload: { ...base, message: '' }, expectedStatus }) | ||
121 | await server.logs.createLogClient({ payload: { ...base, message: 'm'.repeat(2500) }, expectedStatus }) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with an invalid url', async function () { | ||
125 | await server.logs.createLogClient({ payload: { ...base, url: undefined }, expectedStatus }) | ||
126 | await server.logs.createLogClient({ payload: { ...base, url: 'toto' }, expectedStatus }) | ||
127 | }) | ||
128 | |||
129 | it('Should fail with an invalid stackTrace', async function () { | ||
130 | await server.logs.createLogClient({ payload: { ...base, stackTrace: 's'.repeat(20000) }, expectedStatus }) | ||
131 | }) | ||
132 | |||
133 | it('Should fail with an invalid userAgent', async function () { | ||
134 | await server.logs.createLogClient({ payload: { ...base, userAgent: 's'.repeat(500) }, expectedStatus }) | ||
135 | }) | ||
136 | |||
137 | it('Should fail with an invalid meta', async function () { | ||
138 | await server.logs.createLogClient({ payload: { ...base, meta: 's'.repeat(10000) }, expectedStatus }) | ||
139 | }) | ||
140 | |||
141 | it('Should succeed with the correct params', async function () { | ||
142 | await server.logs.createLogClient({ payload: { ...base, stackTrace: 'stackTrace', meta: '{toto}', userAgent: 'userAgent' } }) | ||
143 | }) | ||
144 | |||
145 | it('Should rate limit log creation', async function () { | ||
146 | let fail = false | ||
147 | |||
148 | for (let i = 0; i < 10; i++) { | ||
149 | try { | ||
150 | await server.logs.createLogClient({ token: null, payload: base }) | ||
151 | } catch { | ||
152 | fail = true | ||
153 | } | ||
154 | } | ||
155 | |||
156 | expect(fail).to.be.true | ||
157 | }) | ||
158 | }) | ||
159 | |||
160 | after(async function () { | ||
161 | await cleanupTests([ server ]) | ||
162 | }) | ||
163 | }) | ||
diff --git a/packages/tests/src/api/check-params/metrics.ts b/packages/tests/src/api/check-params/metrics.ts new file mode 100644 index 000000000..cda854554 --- /dev/null +++ b/packages/tests/src/api/check-params/metrics.ts | |||
@@ -0,0 +1,214 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { omit } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode, PlaybackMetricCreate, VideoResolution } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makePostBodyRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test metrics API validators', function () { | ||
14 | let server: PeerTubeServer | ||
15 | let videoUUID: string | ||
16 | |||
17 | // --------------------------------------------------------------- | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(120000) | ||
21 | |||
22 | server = await createSingleServer(1, { | ||
23 | open_telemetry: { | ||
24 | metrics: { | ||
25 | enabled: true | ||
26 | } | ||
27 | } | ||
28 | }) | ||
29 | |||
30 | await setAccessTokensToServers([ server ]) | ||
31 | |||
32 | const { uuid } = await server.videos.quickUpload({ name: 'video' }) | ||
33 | videoUUID = uuid | ||
34 | }) | ||
35 | |||
36 | describe('When adding playback metrics', function () { | ||
37 | const path = '/api/v1/metrics/playback' | ||
38 | let baseParams: PlaybackMetricCreate | ||
39 | |||
40 | before(function () { | ||
41 | baseParams = { | ||
42 | playerMode: 'p2p-media-loader', | ||
43 | resolution: VideoResolution.H_1080P, | ||
44 | fps: 30, | ||
45 | resolutionChanges: 1, | ||
46 | errors: 2, | ||
47 | p2pEnabled: true, | ||
48 | downloadedBytesP2P: 0, | ||
49 | downloadedBytesHTTP: 0, | ||
50 | uploadedBytesP2P: 0, | ||
51 | videoId: videoUUID | ||
52 | } | ||
53 | }) | ||
54 | |||
55 | it('Should fail with an invalid resolution', async function () { | ||
56 | await makePostBodyRequest({ | ||
57 | url: server.url, | ||
58 | path, | ||
59 | fields: { ...baseParams, resolution: 'toto' } | ||
60 | }) | ||
61 | }) | ||
62 | |||
63 | it('Should fail with an invalid fps', async function () { | ||
64 | await makePostBodyRequest({ | ||
65 | url: server.url, | ||
66 | path, | ||
67 | fields: { ...baseParams, fps: 'toto' } | ||
68 | }) | ||
69 | }) | ||
70 | |||
71 | it('Should fail with a missing/invalid player mode', async function () { | ||
72 | await makePostBodyRequest({ | ||
73 | url: server.url, | ||
74 | path, | ||
75 | fields: omit(baseParams, [ 'playerMode' ]) | ||
76 | }) | ||
77 | |||
78 | await makePostBodyRequest({ | ||
79 | url: server.url, | ||
80 | path, | ||
81 | fields: { ...baseParams, playerMode: 'toto' } | ||
82 | }) | ||
83 | }) | ||
84 | |||
85 | it('Should fail with an missing/invalid resolution changes', async function () { | ||
86 | await makePostBodyRequest({ | ||
87 | url: server.url, | ||
88 | path, | ||
89 | fields: omit(baseParams, [ 'resolutionChanges' ]) | ||
90 | }) | ||
91 | |||
92 | await makePostBodyRequest({ | ||
93 | url: server.url, | ||
94 | path, | ||
95 | fields: { ...baseParams, resolutionChanges: 'toto' } | ||
96 | }) | ||
97 | }) | ||
98 | |||
99 | it('Should fail with an missing/invalid errors', async function () { | ||
100 | await makePostBodyRequest({ | ||
101 | url: server.url, | ||
102 | path, | ||
103 | fields: omit(baseParams, [ 'errors' ]) | ||
104 | }) | ||
105 | |||
106 | await makePostBodyRequest({ | ||
107 | url: server.url, | ||
108 | path, | ||
109 | fields: { ...baseParams, errors: 'toto' } | ||
110 | }) | ||
111 | }) | ||
112 | |||
113 | it('Should fail with an missing/invalid downloadedBytesP2P', async function () { | ||
114 | await makePostBodyRequest({ | ||
115 | url: server.url, | ||
116 | path, | ||
117 | fields: omit(baseParams, [ 'downloadedBytesP2P' ]) | ||
118 | }) | ||
119 | |||
120 | await makePostBodyRequest({ | ||
121 | url: server.url, | ||
122 | path, | ||
123 | fields: { ...baseParams, downloadedBytesP2P: 'toto' } | ||
124 | }) | ||
125 | }) | ||
126 | |||
127 | it('Should fail with an missing/invalid downloadedBytesHTTP', async function () { | ||
128 | await makePostBodyRequest({ | ||
129 | url: server.url, | ||
130 | path, | ||
131 | fields: omit(baseParams, [ 'downloadedBytesHTTP' ]) | ||
132 | }) | ||
133 | |||
134 | await makePostBodyRequest({ | ||
135 | url: server.url, | ||
136 | path, | ||
137 | fields: { ...baseParams, downloadedBytesHTTP: 'toto' } | ||
138 | }) | ||
139 | }) | ||
140 | |||
141 | it('Should fail with an missing/invalid uploadedBytesP2P', async function () { | ||
142 | await makePostBodyRequest({ | ||
143 | url: server.url, | ||
144 | path, | ||
145 | fields: omit(baseParams, [ 'uploadedBytesP2P' ]) | ||
146 | }) | ||
147 | |||
148 | await makePostBodyRequest({ | ||
149 | url: server.url, | ||
150 | path, | ||
151 | fields: { ...baseParams, uploadedBytesP2P: 'toto' } | ||
152 | }) | ||
153 | }) | ||
154 | |||
155 | it('Should fail with a missing/invalid p2pEnabled', async function () { | ||
156 | await makePostBodyRequest({ | ||
157 | url: server.url, | ||
158 | path, | ||
159 | fields: omit(baseParams, [ 'p2pEnabled' ]) | ||
160 | }) | ||
161 | |||
162 | await makePostBodyRequest({ | ||
163 | url: server.url, | ||
164 | path, | ||
165 | fields: { ...baseParams, p2pEnabled: 'toto' } | ||
166 | }) | ||
167 | }) | ||
168 | |||
169 | it('Should fail with an invalid totalPeers', async function () { | ||
170 | await makePostBodyRequest({ | ||
171 | url: server.url, | ||
172 | path, | ||
173 | fields: { ...baseParams, p2pPeers: 'toto' } | ||
174 | }) | ||
175 | }) | ||
176 | |||
177 | it('Should fail with a bad video id', async function () { | ||
178 | await makePostBodyRequest({ | ||
179 | url: server.url, | ||
180 | path, | ||
181 | fields: { ...baseParams, videoId: 'toto' } | ||
182 | }) | ||
183 | }) | ||
184 | |||
185 | it('Should fail with an unknown video', async function () { | ||
186 | await makePostBodyRequest({ | ||
187 | url: server.url, | ||
188 | path, | ||
189 | fields: { ...baseParams, videoId: 42 }, | ||
190 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
191 | }) | ||
192 | }) | ||
193 | |||
194 | it('Should succeed with the correct params', async function () { | ||
195 | await makePostBodyRequest({ | ||
196 | url: server.url, | ||
197 | path, | ||
198 | fields: baseParams, | ||
199 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
200 | }) | ||
201 | |||
202 | await makePostBodyRequest({ | ||
203 | url: server.url, | ||
204 | path, | ||
205 | fields: { ...baseParams, p2pEnabled: false, totalPeers: 32 }, | ||
206 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
207 | }) | ||
208 | }) | ||
209 | }) | ||
210 | |||
211 | after(async function () { | ||
212 | await cleanupTests([ server ]) | ||
213 | }) | ||
214 | }) | ||
diff --git a/packages/tests/src/api/check-params/my-user.ts b/packages/tests/src/api/check-params/my-user.ts new file mode 100644 index 000000000..2ef2e242a --- /dev/null +++ b/packages/tests/src/api/check-params/my-user.ts | |||
@@ -0,0 +1,492 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
6 | import { HttpStatusCode, UserRole, VideoCreateResult } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | makeGetRequest, | ||
11 | makePutBodyRequest, | ||
12 | makeUploadRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | UsersCommand | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('Test my user API validators', function () { | ||
19 | const path = '/api/v1/users/' | ||
20 | let userId: number | ||
21 | let rootId: number | ||
22 | let moderatorId: number | ||
23 | let video: VideoCreateResult | ||
24 | let server: PeerTubeServer | ||
25 | let userToken = '' | ||
26 | let moderatorToken = '' | ||
27 | |||
28 | // --------------------------------------------------------------- | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(30000) | ||
32 | |||
33 | { | ||
34 | server = await createSingleServer(1) | ||
35 | await setAccessTokensToServers([ server ]) | ||
36 | } | ||
37 | |||
38 | { | ||
39 | const result = await server.users.generate('user1') | ||
40 | userToken = result.token | ||
41 | userId = result.userId | ||
42 | } | ||
43 | |||
44 | { | ||
45 | const result = await server.users.generate('moderator1', UserRole.MODERATOR) | ||
46 | moderatorToken = result.token | ||
47 | } | ||
48 | |||
49 | { | ||
50 | const result = await server.users.generate('moderator2', UserRole.MODERATOR) | ||
51 | moderatorId = result.userId | ||
52 | } | ||
53 | |||
54 | { | ||
55 | video = await server.videos.upload() | ||
56 | } | ||
57 | }) | ||
58 | |||
59 | describe('When updating my account', function () { | ||
60 | |||
61 | it('Should fail with an invalid email attribute', async function () { | ||
62 | const fields = { | ||
63 | email: 'blabla' | ||
64 | } | ||
65 | |||
66 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: server.accessToken, fields }) | ||
67 | }) | ||
68 | |||
69 | it('Should fail with a too small password', async function () { | ||
70 | const fields = { | ||
71 | currentPassword: 'password', | ||
72 | password: 'bla' | ||
73 | } | ||
74 | |||
75 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
76 | }) | ||
77 | |||
78 | it('Should fail with a too long password', async function () { | ||
79 | const fields = { | ||
80 | currentPassword: 'password', | ||
81 | password: 'super'.repeat(61) | ||
82 | } | ||
83 | |||
84 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
85 | }) | ||
86 | |||
87 | it('Should fail without the current password', async function () { | ||
88 | const fields = { | ||
89 | currentPassword: 'password', | ||
90 | password: 'super'.repeat(61) | ||
91 | } | ||
92 | |||
93 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
94 | }) | ||
95 | |||
96 | it('Should fail with an invalid current password', async function () { | ||
97 | const fields = { | ||
98 | currentPassword: 'my super password fail', | ||
99 | password: 'super'.repeat(61) | ||
100 | } | ||
101 | |||
102 | await makePutBodyRequest({ | ||
103 | url: server.url, | ||
104 | path: path + 'me', | ||
105 | token: userToken, | ||
106 | fields, | ||
107 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
108 | }) | ||
109 | }) | ||
110 | |||
111 | it('Should fail with an invalid NSFW policy attribute', async function () { | ||
112 | const fields = { | ||
113 | nsfwPolicy: 'hello' | ||
114 | } | ||
115 | |||
116 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
117 | }) | ||
118 | |||
119 | it('Should fail with an invalid autoPlayVideo attribute', async function () { | ||
120 | const fields = { | ||
121 | autoPlayVideo: -1 | ||
122 | } | ||
123 | |||
124 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
125 | }) | ||
126 | |||
127 | it('Should fail with an invalid autoPlayNextVideo attribute', async function () { | ||
128 | const fields = { | ||
129 | autoPlayNextVideo: -1 | ||
130 | } | ||
131 | |||
132 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
133 | }) | ||
134 | |||
135 | it('Should fail with an invalid videosHistoryEnabled attribute', async function () { | ||
136 | const fields = { | ||
137 | videosHistoryEnabled: -1 | ||
138 | } | ||
139 | |||
140 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
141 | }) | ||
142 | |||
143 | it('Should fail with an non authenticated user', async function () { | ||
144 | const fields = { | ||
145 | currentPassword: 'password', | ||
146 | password: 'my super password' | ||
147 | } | ||
148 | |||
149 | await makePutBodyRequest({ | ||
150 | url: server.url, | ||
151 | path: path + 'me', | ||
152 | token: 'super token', | ||
153 | fields, | ||
154 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
155 | }) | ||
156 | }) | ||
157 | |||
158 | it('Should fail with a too long description', async function () { | ||
159 | const fields = { | ||
160 | description: 'super'.repeat(201) | ||
161 | } | ||
162 | |||
163 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
164 | }) | ||
165 | |||
166 | it('Should fail with an invalid videoLanguages attribute', async function () { | ||
167 | { | ||
168 | const fields = { | ||
169 | videoLanguages: 'toto' | ||
170 | } | ||
171 | |||
172 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
173 | } | ||
174 | |||
175 | { | ||
176 | const languages = [] | ||
177 | for (let i = 0; i < 1000; i++) { | ||
178 | languages.push('fr') | ||
179 | } | ||
180 | |||
181 | const fields = { | ||
182 | videoLanguages: languages | ||
183 | } | ||
184 | |||
185 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
186 | } | ||
187 | }) | ||
188 | |||
189 | it('Should fail with an invalid theme', async function () { | ||
190 | const fields = { theme: 'invalid' } | ||
191 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
192 | }) | ||
193 | |||
194 | it('Should fail with an unknown theme', async function () { | ||
195 | const fields = { theme: 'peertube-theme-unknown' } | ||
196 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
197 | }) | ||
198 | |||
199 | it('Should fail with invalid no modal attributes', async function () { | ||
200 | const keys = [ | ||
201 | 'noInstanceConfigWarningModal', | ||
202 | 'noAccountSetupWarningModal', | ||
203 | 'noWelcomeModal' | ||
204 | ] | ||
205 | |||
206 | for (const key of keys) { | ||
207 | const fields = { | ||
208 | [key]: -1 | ||
209 | } | ||
210 | |||
211 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) | ||
212 | } | ||
213 | }) | ||
214 | |||
215 | it('Should succeed to change password with the correct params', async function () { | ||
216 | const fields = { | ||
217 | currentPassword: 'password', | ||
218 | password: 'my super password', | ||
219 | nsfwPolicy: 'blur', | ||
220 | autoPlayVideo: false, | ||
221 | email: 'super_email@example.com', | ||
222 | theme: 'default', | ||
223 | noInstanceConfigWarningModal: true, | ||
224 | noWelcomeModal: true, | ||
225 | noAccountSetupWarningModal: true | ||
226 | } | ||
227 | |||
228 | await makePutBodyRequest({ | ||
229 | url: server.url, | ||
230 | path: path + 'me', | ||
231 | token: userToken, | ||
232 | fields, | ||
233 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
234 | }) | ||
235 | }) | ||
236 | |||
237 | it('Should succeed without password change with the correct params', async function () { | ||
238 | const fields = { | ||
239 | nsfwPolicy: 'blur', | ||
240 | autoPlayVideo: false | ||
241 | } | ||
242 | |||
243 | await makePutBodyRequest({ | ||
244 | url: server.url, | ||
245 | path: path + 'me', | ||
246 | token: userToken, | ||
247 | fields, | ||
248 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
249 | }) | ||
250 | }) | ||
251 | }) | ||
252 | |||
253 | describe('When updating my avatar', function () { | ||
254 | it('Should fail without an incorrect input file', async function () { | ||
255 | const fields = {} | ||
256 | const attaches = { | ||
257 | avatarfile: buildAbsoluteFixturePath('video_short.mp4') | ||
258 | } | ||
259 | await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) | ||
260 | }) | ||
261 | |||
262 | it('Should fail with a big file', async function () { | ||
263 | const fields = {} | ||
264 | const attaches = { | ||
265 | avatarfile: buildAbsoluteFixturePath('avatar-big.png') | ||
266 | } | ||
267 | await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) | ||
268 | }) | ||
269 | |||
270 | it('Should fail with an unauthenticated user', async function () { | ||
271 | const fields = {} | ||
272 | const attaches = { | ||
273 | avatarfile: buildAbsoluteFixturePath('avatar.png') | ||
274 | } | ||
275 | await makeUploadRequest({ | ||
276 | url: server.url, | ||
277 | path: path + '/me/avatar/pick', | ||
278 | fields, | ||
279 | attaches, | ||
280 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
281 | }) | ||
282 | }) | ||
283 | |||
284 | it('Should succeed with the correct params', async function () { | ||
285 | const fields = {} | ||
286 | const attaches = { | ||
287 | avatarfile: buildAbsoluteFixturePath('avatar.png') | ||
288 | } | ||
289 | await makeUploadRequest({ | ||
290 | url: server.url, | ||
291 | path: path + '/me/avatar/pick', | ||
292 | token: server.accessToken, | ||
293 | fields, | ||
294 | attaches, | ||
295 | expectedStatus: HttpStatusCode.OK_200 | ||
296 | }) | ||
297 | }) | ||
298 | }) | ||
299 | |||
300 | describe('When managing my scoped tokens', function () { | ||
301 | |||
302 | it('Should fail to get my scoped tokens with an non authenticated user', async function () { | ||
303 | await server.users.getMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
304 | }) | ||
305 | |||
306 | it('Should fail to get my scoped tokens with a bad token', async function () { | ||
307 | await server.users.getMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
308 | |||
309 | }) | ||
310 | |||
311 | it('Should succeed to get my scoped tokens', async function () { | ||
312 | await server.users.getMyScopedTokens() | ||
313 | }) | ||
314 | |||
315 | it('Should fail to renew my scoped tokens with an non authenticated user', async function () { | ||
316 | await server.users.renewMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
317 | }) | ||
318 | |||
319 | it('Should fail to renew my scoped tokens with a bad token', async function () { | ||
320 | await server.users.renewMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
321 | }) | ||
322 | |||
323 | it('Should succeed to renew my scoped tokens', async function () { | ||
324 | await server.users.renewMyScopedTokens() | ||
325 | }) | ||
326 | }) | ||
327 | |||
328 | describe('When getting my information', function () { | ||
329 | it('Should fail with a non authenticated user', async function () { | ||
330 | await server.users.getMyInfo({ token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
331 | }) | ||
332 | |||
333 | it('Should success with the correct parameters', async function () { | ||
334 | await server.users.getMyInfo({ token: userToken }) | ||
335 | }) | ||
336 | }) | ||
337 | |||
338 | describe('When getting my video rating', function () { | ||
339 | let command: UsersCommand | ||
340 | |||
341 | before(function () { | ||
342 | command = server.users | ||
343 | }) | ||
344 | |||
345 | it('Should fail with a non authenticated user', async function () { | ||
346 | await command.getMyRating({ token: 'fake_token', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
347 | }) | ||
348 | |||
349 | it('Should fail with an incorrect video uuid', async function () { | ||
350 | await command.getMyRating({ videoId: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
351 | }) | ||
352 | |||
353 | it('Should fail with an unknown video', async function () { | ||
354 | await command.getMyRating({ videoId: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
355 | }) | ||
356 | |||
357 | it('Should succeed with the correct parameters', async function () { | ||
358 | await command.getMyRating({ videoId: video.id }) | ||
359 | await command.getMyRating({ videoId: video.uuid }) | ||
360 | await command.getMyRating({ videoId: video.shortUUID }) | ||
361 | }) | ||
362 | }) | ||
363 | |||
364 | describe('When retrieving my global ratings', function () { | ||
365 | const path = '/api/v1/accounts/user1/ratings' | ||
366 | |||
367 | it('Should fail with a bad start pagination', async function () { | ||
368 | await checkBadStartPagination(server.url, path, userToken) | ||
369 | }) | ||
370 | |||
371 | it('Should fail with a bad count pagination', async function () { | ||
372 | await checkBadCountPagination(server.url, path, userToken) | ||
373 | }) | ||
374 | |||
375 | it('Should fail with an incorrect sort', async function () { | ||
376 | await checkBadSortPagination(server.url, path, userToken) | ||
377 | }) | ||
378 | |||
379 | it('Should fail with a unauthenticated user', async function () { | ||
380 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
381 | }) | ||
382 | |||
383 | it('Should fail with a another user', async function () { | ||
384 | await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
385 | }) | ||
386 | |||
387 | it('Should fail with a bad type', async function () { | ||
388 | await makeGetRequest({ | ||
389 | url: server.url, | ||
390 | path, | ||
391 | token: userToken, | ||
392 | query: { rating: 'toto ' }, | ||
393 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
394 | }) | ||
395 | }) | ||
396 | |||
397 | it('Should succeed with the correct params', async function () { | ||
398 | await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
399 | }) | ||
400 | }) | ||
401 | |||
402 | describe('When getting my global followers', function () { | ||
403 | const path = '/api/v1/accounts/user1/followers' | ||
404 | |||
405 | it('Should fail with a bad start pagination', async function () { | ||
406 | await checkBadStartPagination(server.url, path, userToken) | ||
407 | }) | ||
408 | |||
409 | it('Should fail with a bad count pagination', async function () { | ||
410 | await checkBadCountPagination(server.url, path, userToken) | ||
411 | }) | ||
412 | |||
413 | it('Should fail with an incorrect sort', async function () { | ||
414 | await checkBadSortPagination(server.url, path, userToken) | ||
415 | }) | ||
416 | |||
417 | it('Should fail with a unauthenticated user', async function () { | ||
418 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
419 | }) | ||
420 | |||
421 | it('Should fail with a another user', async function () { | ||
422 | await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
423 | }) | ||
424 | |||
425 | it('Should succeed with the correct params', async function () { | ||
426 | await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
427 | }) | ||
428 | }) | ||
429 | |||
430 | describe('When blocking/unblocking/removing user', function () { | ||
431 | |||
432 | it('Should fail with an incorrect id', async function () { | ||
433 | const options = { userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
434 | |||
435 | await server.users.remove(options) | ||
436 | await server.users.banUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
437 | await server.users.unbanUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
438 | }) | ||
439 | |||
440 | it('Should fail with the root user', async function () { | ||
441 | const options = { userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
442 | |||
443 | await server.users.remove(options) | ||
444 | await server.users.banUser(options) | ||
445 | await server.users.unbanUser(options) | ||
446 | }) | ||
447 | |||
448 | it('Should return 404 with a non existing id', async function () { | ||
449 | const options = { userId: 4545454, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
450 | |||
451 | await server.users.remove(options) | ||
452 | await server.users.banUser(options) | ||
453 | await server.users.unbanUser(options) | ||
454 | }) | ||
455 | |||
456 | it('Should fail with a non admin user', async function () { | ||
457 | const options = { userId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 } | ||
458 | |||
459 | await server.users.remove(options) | ||
460 | await server.users.banUser(options) | ||
461 | await server.users.unbanUser(options) | ||
462 | }) | ||
463 | |||
464 | it('Should fail on a moderator with a moderator', async function () { | ||
465 | const options = { userId: moderatorId, token: moderatorToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 } | ||
466 | |||
467 | await server.users.remove(options) | ||
468 | await server.users.banUser(options) | ||
469 | await server.users.unbanUser(options) | ||
470 | }) | ||
471 | |||
472 | it('Should succeed on a user with a moderator', async function () { | ||
473 | const options = { userId, token: moderatorToken } | ||
474 | |||
475 | await server.users.banUser(options) | ||
476 | await server.users.unbanUser(options) | ||
477 | }) | ||
478 | }) | ||
479 | |||
480 | describe('When deleting our account', function () { | ||
481 | |||
482 | it('Should fail with with the root account', async function () { | ||
483 | await server.users.deleteMe({ expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
484 | }) | ||
485 | }) | ||
486 | |||
487 | after(async function () { | ||
488 | MockSmtpServer.Instance.kill() | ||
489 | |||
490 | await cleanupTests([ server ]) | ||
491 | }) | ||
492 | }) | ||
diff --git a/packages/tests/src/api/check-params/plugins.ts b/packages/tests/src/api/check-params/plugins.ts new file mode 100644 index 000000000..ab2a426fe --- /dev/null +++ b/packages/tests/src/api/check-params/plugins.ts | |||
@@ -0,0 +1,490 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode, PeerTubePlugin, PluginType } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeGetRequest, | ||
9 | makePostBodyRequest, | ||
10 | makePutBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test server plugins API validators', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let userAccessToken = null | ||
18 | |||
19 | const npmPlugin = 'peertube-plugin-hello-world' | ||
20 | const pluginName = 'hello-world' | ||
21 | let npmVersion: string | ||
22 | |||
23 | const themePlugin = 'peertube-theme-background-red' | ||
24 | const themeName = 'background-red' | ||
25 | let themeVersion: string | ||
26 | |||
27 | // --------------------------------------------------------------- | ||
28 | |||
29 | before(async function () { | ||
30 | this.timeout(60000) | ||
31 | |||
32 | server = await createSingleServer(1) | ||
33 | |||
34 | await setAccessTokensToServers([ server ]) | ||
35 | |||
36 | const user = { | ||
37 | username: 'user1', | ||
38 | password: 'password' | ||
39 | } | ||
40 | |||
41 | await server.users.create({ username: user.username, password: user.password }) | ||
42 | userAccessToken = await server.login.getAccessToken(user) | ||
43 | |||
44 | { | ||
45 | const res = await server.plugins.install({ npmName: npmPlugin }) | ||
46 | const plugin = res.body as PeerTubePlugin | ||
47 | npmVersion = plugin.version | ||
48 | } | ||
49 | |||
50 | { | ||
51 | const res = await server.plugins.install({ npmName: themePlugin }) | ||
52 | const plugin = res.body as PeerTubePlugin | ||
53 | themeVersion = plugin.version | ||
54 | } | ||
55 | }) | ||
56 | |||
57 | describe('With static plugin routes', function () { | ||
58 | it('Should fail with an unknown plugin name/plugin version', async function () { | ||
59 | const paths = [ | ||
60 | '/plugins/' + pluginName + '/0.0.1/auth/fake-auth', | ||
61 | '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png', | ||
62 | '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js', | ||
63 | '/themes/' + themeName + '/0.0.1/static/images/chocobo.png', | ||
64 | '/themes/' + themeName + '/0.0.1/client-scripts/client/video-watch-client-plugin.js', | ||
65 | '/themes/' + themeName + '/0.0.1/css/assets/style1.css' | ||
66 | ] | ||
67 | |||
68 | for (const p of paths) { | ||
69 | await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
70 | } | ||
71 | }) | ||
72 | |||
73 | it('Should fail when requesting a plugin in the theme path', async function () { | ||
74 | await makeGetRequest({ | ||
75 | url: server.url, | ||
76 | path: '/themes/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png', | ||
77 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
78 | }) | ||
79 | }) | ||
80 | |||
81 | it('Should fail with invalid versions', async function () { | ||
82 | const paths = [ | ||
83 | '/plugins/' + pluginName + '/0.0.1.1/auth/fake-auth', | ||
84 | '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png', | ||
85 | '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js', | ||
86 | '/themes/' + themeName + '/1/static/images/chocobo.png', | ||
87 | '/themes/' + themeName + '/0.0.1000a/client-scripts/client/video-watch-client-plugin.js', | ||
88 | '/themes/' + themeName + '/0.a.1/css/assets/style1.css' | ||
89 | ] | ||
90 | |||
91 | for (const p of paths) { | ||
92 | await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
93 | } | ||
94 | }) | ||
95 | |||
96 | it('Should fail with invalid paths', async function () { | ||
97 | const paths = [ | ||
98 | '/plugins/' + pluginName + '/' + npmVersion + '/static/images/../chocobo.png', | ||
99 | '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/../client/common-client-plugin.js', | ||
100 | '/themes/' + themeName + '/' + themeVersion + '/static/../images/chocobo.png', | ||
101 | '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js/..', | ||
102 | '/themes/' + themeName + '/' + themeVersion + '/css/../assets/style1.css' | ||
103 | ] | ||
104 | |||
105 | for (const p of paths) { | ||
106 | await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
107 | } | ||
108 | }) | ||
109 | |||
110 | it('Should fail with an unknown auth name', async function () { | ||
111 | const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth' | ||
112 | |||
113 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
114 | }) | ||
115 | |||
116 | it('Should fail with an unknown static file', async function () { | ||
117 | const paths = [ | ||
118 | '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png', | ||
119 | '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/fake.js', | ||
120 | '/themes/' + themeName + '/' + themeVersion + '/static/fake/chocobo.png', | ||
121 | '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/fake.js' | ||
122 | ] | ||
123 | |||
124 | for (const p of paths) { | ||
125 | await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
126 | } | ||
127 | }) | ||
128 | |||
129 | it('Should fail with an unknown CSS file', async function () { | ||
130 | await makeGetRequest({ | ||
131 | url: server.url, | ||
132 | path: '/themes/' + themeName + '/' + themeVersion + '/css/assets/fake.css', | ||
133 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
134 | }) | ||
135 | }) | ||
136 | |||
137 | it('Should succeed with the correct parameters', async function () { | ||
138 | const paths = [ | ||
139 | '/plugins/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png', | ||
140 | '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/common-client-plugin.js', | ||
141 | '/themes/' + themeName + '/' + themeVersion + '/static/images/chocobo.png', | ||
142 | '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js', | ||
143 | '/themes/' + themeName + '/' + themeVersion + '/css/assets/style1.css' | ||
144 | ] | ||
145 | |||
146 | for (const p of paths) { | ||
147 | await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.OK_200 }) | ||
148 | } | ||
149 | |||
150 | const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth' | ||
151 | await makeGetRequest({ url: server.url, path: authPath, expectedStatus: HttpStatusCode.FOUND_302 }) | ||
152 | }) | ||
153 | }) | ||
154 | |||
155 | describe('When listing available plugins/themes', function () { | ||
156 | const path = '/api/v1/plugins/available' | ||
157 | const baseQuery = { | ||
158 | search: 'super search', | ||
159 | pluginType: PluginType.PLUGIN, | ||
160 | currentPeerTubeEngine: '1.2.3' | ||
161 | } | ||
162 | |||
163 | it('Should fail with an invalid token', async function () { | ||
164 | await makeGetRequest({ | ||
165 | url: server.url, | ||
166 | path, | ||
167 | token: 'fake_token', | ||
168 | query: baseQuery, | ||
169 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
170 | }) | ||
171 | }) | ||
172 | |||
173 | it('Should fail if the user is not an administrator', async function () { | ||
174 | await makeGetRequest({ | ||
175 | url: server.url, | ||
176 | path, | ||
177 | token: userAccessToken, | ||
178 | query: baseQuery, | ||
179 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
180 | }) | ||
181 | }) | ||
182 | |||
183 | it('Should fail with a bad start pagination', async function () { | ||
184 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
185 | }) | ||
186 | |||
187 | it('Should fail with a bad count pagination', async function () { | ||
188 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
189 | }) | ||
190 | |||
191 | it('Should fail with an incorrect sort', async function () { | ||
192 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
193 | }) | ||
194 | |||
195 | it('Should fail with an invalid plugin type', async function () { | ||
196 | const query = { ...baseQuery, pluginType: 5 } | ||
197 | |||
198 | await makeGetRequest({ | ||
199 | url: server.url, | ||
200 | path, | ||
201 | token: server.accessToken, | ||
202 | query | ||
203 | }) | ||
204 | }) | ||
205 | |||
206 | it('Should fail with an invalid current peertube engine', async function () { | ||
207 | const query = { ...baseQuery, currentPeerTubeEngine: '1.0' } | ||
208 | |||
209 | await makeGetRequest({ | ||
210 | url: server.url, | ||
211 | path, | ||
212 | token: server.accessToken, | ||
213 | query | ||
214 | }) | ||
215 | }) | ||
216 | |||
217 | it('Should success with the correct parameters', async function () { | ||
218 | await makeGetRequest({ | ||
219 | url: server.url, | ||
220 | path, | ||
221 | token: server.accessToken, | ||
222 | query: baseQuery, | ||
223 | expectedStatus: HttpStatusCode.OK_200 | ||
224 | }) | ||
225 | }) | ||
226 | }) | ||
227 | |||
228 | describe('When listing local plugins/themes', function () { | ||
229 | const path = '/api/v1/plugins' | ||
230 | const baseQuery = { | ||
231 | pluginType: PluginType.THEME | ||
232 | } | ||
233 | |||
234 | it('Should fail with an invalid token', async function () { | ||
235 | await makeGetRequest({ | ||
236 | url: server.url, | ||
237 | path, | ||
238 | token: 'fake_token', | ||
239 | query: baseQuery, | ||
240 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
241 | }) | ||
242 | }) | ||
243 | |||
244 | it('Should fail if the user is not an administrator', async function () { | ||
245 | await makeGetRequest({ | ||
246 | url: server.url, | ||
247 | path, | ||
248 | token: userAccessToken, | ||
249 | query: baseQuery, | ||
250 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
251 | }) | ||
252 | }) | ||
253 | |||
254 | it('Should fail with a bad start pagination', async function () { | ||
255 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
256 | }) | ||
257 | |||
258 | it('Should fail with a bad count pagination', async function () { | ||
259 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
260 | }) | ||
261 | |||
262 | it('Should fail with an incorrect sort', async function () { | ||
263 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
264 | }) | ||
265 | |||
266 | it('Should fail with an invalid plugin type', async function () { | ||
267 | const query = { ...baseQuery, pluginType: 5 } | ||
268 | |||
269 | await makeGetRequest({ | ||
270 | url: server.url, | ||
271 | path, | ||
272 | token: server.accessToken, | ||
273 | query | ||
274 | }) | ||
275 | }) | ||
276 | |||
277 | it('Should success with the correct parameters', async function () { | ||
278 | await makeGetRequest({ | ||
279 | url: server.url, | ||
280 | path, | ||
281 | token: server.accessToken, | ||
282 | query: baseQuery, | ||
283 | expectedStatus: HttpStatusCode.OK_200 | ||
284 | }) | ||
285 | }) | ||
286 | }) | ||
287 | |||
288 | describe('When getting a plugin or the registered settings or public settings', function () { | ||
289 | const path = '/api/v1/plugins/' | ||
290 | |||
291 | it('Should fail with an invalid token', async function () { | ||
292 | for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) { | ||
293 | await makeGetRequest({ | ||
294 | url: server.url, | ||
295 | path: path + suffix, | ||
296 | token: 'fake_token', | ||
297 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
298 | }) | ||
299 | } | ||
300 | }) | ||
301 | |||
302 | it('Should fail if the user is not an administrator', async function () { | ||
303 | for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) { | ||
304 | await makeGetRequest({ | ||
305 | url: server.url, | ||
306 | path: path + suffix, | ||
307 | token: userAccessToken, | ||
308 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
309 | }) | ||
310 | } | ||
311 | }) | ||
312 | |||
313 | it('Should fail with an invalid npm name', async function () { | ||
314 | for (const suffix of [ 'toto', 'toto/registered-settings', 'toto/public-settings' ]) { | ||
315 | await makeGetRequest({ | ||
316 | url: server.url, | ||
317 | path: path + suffix, | ||
318 | token: server.accessToken, | ||
319 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
320 | }) | ||
321 | } | ||
322 | |||
323 | for (const suffix of [ 'peertube-plugin-TOTO', 'peertube-plugin-TOTO/registered-settings', 'peertube-plugin-TOTO/public-settings' ]) { | ||
324 | await makeGetRequest({ | ||
325 | url: server.url, | ||
326 | path: path + suffix, | ||
327 | token: server.accessToken, | ||
328 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
329 | }) | ||
330 | } | ||
331 | }) | ||
332 | |||
333 | it('Should fail with an unknown plugin', async function () { | ||
334 | for (const suffix of [ 'peertube-plugin-toto', 'peertube-plugin-toto/registered-settings', 'peertube-plugin-toto/public-settings' ]) { | ||
335 | await makeGetRequest({ | ||
336 | url: server.url, | ||
337 | path: path + suffix, | ||
338 | token: server.accessToken, | ||
339 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
340 | }) | ||
341 | } | ||
342 | }) | ||
343 | |||
344 | it('Should succeed with the correct parameters', async function () { | ||
345 | for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings`, `${npmPlugin}/public-settings` ]) { | ||
346 | await makeGetRequest({ | ||
347 | url: server.url, | ||
348 | path: path + suffix, | ||
349 | token: server.accessToken, | ||
350 | expectedStatus: HttpStatusCode.OK_200 | ||
351 | }) | ||
352 | } | ||
353 | }) | ||
354 | }) | ||
355 | |||
356 | describe('When updating plugin settings', function () { | ||
357 | const path = '/api/v1/plugins/' | ||
358 | const settings = { setting1: 'value1' } | ||
359 | |||
360 | it('Should fail with an invalid token', async function () { | ||
361 | await makePutBodyRequest({ | ||
362 | url: server.url, | ||
363 | path: path + npmPlugin + '/settings', | ||
364 | fields: { settings }, | ||
365 | token: 'fake_token', | ||
366 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
367 | }) | ||
368 | }) | ||
369 | |||
370 | it('Should fail if the user is not an administrator', async function () { | ||
371 | await makePutBodyRequest({ | ||
372 | url: server.url, | ||
373 | path: path + npmPlugin + '/settings', | ||
374 | fields: { settings }, | ||
375 | token: userAccessToken, | ||
376 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
377 | }) | ||
378 | }) | ||
379 | |||
380 | it('Should fail with an invalid npm name', async function () { | ||
381 | await makePutBodyRequest({ | ||
382 | url: server.url, | ||
383 | path: path + 'toto/settings', | ||
384 | fields: { settings }, | ||
385 | token: server.accessToken, | ||
386 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
387 | }) | ||
388 | |||
389 | await makePutBodyRequest({ | ||
390 | url: server.url, | ||
391 | path: path + 'peertube-plugin-TOTO/settings', | ||
392 | fields: { settings }, | ||
393 | token: server.accessToken, | ||
394 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
395 | }) | ||
396 | }) | ||
397 | |||
398 | it('Should fail with an unknown plugin', async function () { | ||
399 | await makePutBodyRequest({ | ||
400 | url: server.url, | ||
401 | path: path + 'peertube-plugin-toto/settings', | ||
402 | fields: { settings }, | ||
403 | token: server.accessToken, | ||
404 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
405 | }) | ||
406 | }) | ||
407 | |||
408 | it('Should succeed with the correct parameters', async function () { | ||
409 | await makePutBodyRequest({ | ||
410 | url: server.url, | ||
411 | path: path + npmPlugin + '/settings', | ||
412 | fields: { settings }, | ||
413 | token: server.accessToken, | ||
414 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
415 | }) | ||
416 | }) | ||
417 | }) | ||
418 | |||
419 | describe('When installing/updating/uninstalling a plugin', function () { | ||
420 | const path = '/api/v1/plugins/' | ||
421 | |||
422 | it('Should fail with an invalid token', async function () { | ||
423 | for (const suffix of [ 'install', 'update', 'uninstall' ]) { | ||
424 | await makePostBodyRequest({ | ||
425 | url: server.url, | ||
426 | path: path + suffix, | ||
427 | fields: { npmName: npmPlugin }, | ||
428 | token: 'fake_token', | ||
429 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
430 | }) | ||
431 | } | ||
432 | }) | ||
433 | |||
434 | it('Should fail if the user is not an administrator', async function () { | ||
435 | for (const suffix of [ 'install', 'update', 'uninstall' ]) { | ||
436 | await makePostBodyRequest({ | ||
437 | url: server.url, | ||
438 | path: path + suffix, | ||
439 | fields: { npmName: npmPlugin }, | ||
440 | token: userAccessToken, | ||
441 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
442 | }) | ||
443 | } | ||
444 | }) | ||
445 | |||
446 | it('Should fail with an invalid npm name', async function () { | ||
447 | for (const suffix of [ 'install', 'update', 'uninstall' ]) { | ||
448 | await makePostBodyRequest({ | ||
449 | url: server.url, | ||
450 | path: path + suffix, | ||
451 | fields: { npmName: 'toto' }, | ||
452 | token: server.accessToken, | ||
453 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
454 | }) | ||
455 | } | ||
456 | |||
457 | for (const suffix of [ 'install', 'update', 'uninstall' ]) { | ||
458 | await makePostBodyRequest({ | ||
459 | url: server.url, | ||
460 | path: path + suffix, | ||
461 | fields: { npmName: 'peertube-plugin-TOTO' }, | ||
462 | token: server.accessToken, | ||
463 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
464 | }) | ||
465 | } | ||
466 | }) | ||
467 | |||
468 | it('Should succeed with the correct parameters', async function () { | ||
469 | const it = [ | ||
470 | { suffix: 'install', status: HttpStatusCode.OK_200 }, | ||
471 | { suffix: 'update', status: HttpStatusCode.OK_200 }, | ||
472 | { suffix: 'uninstall', status: HttpStatusCode.NO_CONTENT_204 } | ||
473 | ] | ||
474 | |||
475 | for (const obj of it) { | ||
476 | await makePostBodyRequest({ | ||
477 | url: server.url, | ||
478 | path: path + obj.suffix, | ||
479 | fields: { npmName: npmPlugin }, | ||
480 | token: server.accessToken, | ||
481 | expectedStatus: obj.status | ||
482 | }) | ||
483 | } | ||
484 | }) | ||
485 | }) | ||
486 | |||
487 | after(async function () { | ||
488 | await cleanupTests([ server ]) | ||
489 | }) | ||
490 | }) | ||
diff --git a/packages/tests/src/api/check-params/redundancy.ts b/packages/tests/src/api/check-params/redundancy.ts new file mode 100644 index 000000000..16a5d0a3d --- /dev/null +++ b/packages/tests/src/api/check-params/redundancy.ts | |||
@@ -0,0 +1,240 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode, VideoCreateResult } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | makeDeleteRequest, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | makePutBodyRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | waitJobs | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('Test server redundancy API validators', function () { | ||
19 | let servers: PeerTubeServer[] | ||
20 | let userAccessToken = null | ||
21 | let videoIdLocal: number | ||
22 | let videoRemote: VideoCreateResult | ||
23 | |||
24 | // --------------------------------------------------------------- | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(160000) | ||
28 | |||
29 | servers = await createMultipleServers(2) | ||
30 | |||
31 | await setAccessTokensToServers(servers) | ||
32 | await doubleFollow(servers[0], servers[1]) | ||
33 | |||
34 | const user = { | ||
35 | username: 'user1', | ||
36 | password: 'password' | ||
37 | } | ||
38 | |||
39 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
40 | userAccessToken = await servers[0].login.getAccessToken(user) | ||
41 | |||
42 | videoIdLocal = (await servers[0].videos.quickUpload({ name: 'video' })).id | ||
43 | |||
44 | const remoteUUID = (await servers[1].videos.quickUpload({ name: 'video' })).uuid | ||
45 | |||
46 | await waitJobs(servers) | ||
47 | |||
48 | videoRemote = await servers[0].videos.get({ id: remoteUUID }) | ||
49 | }) | ||
50 | |||
51 | describe('When listing redundancies', function () { | ||
52 | const path = '/api/v1/server/redundancy/videos' | ||
53 | |||
54 | let url: string | ||
55 | let token: string | ||
56 | |||
57 | before(function () { | ||
58 | url = servers[0].url | ||
59 | token = servers[0].accessToken | ||
60 | }) | ||
61 | |||
62 | it('Should fail with an invalid token', async function () { | ||
63 | await makeGetRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
64 | }) | ||
65 | |||
66 | it('Should fail if the user is not an administrator', async function () { | ||
67 | await makeGetRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
68 | }) | ||
69 | |||
70 | it('Should fail with a bad start pagination', async function () { | ||
71 | await checkBadStartPagination(url, path, servers[0].accessToken) | ||
72 | }) | ||
73 | |||
74 | it('Should fail with a bad count pagination', async function () { | ||
75 | await checkBadCountPagination(url, path, servers[0].accessToken) | ||
76 | }) | ||
77 | |||
78 | it('Should fail with an incorrect sort', async function () { | ||
79 | await checkBadSortPagination(url, path, servers[0].accessToken) | ||
80 | }) | ||
81 | |||
82 | it('Should fail with a bad target', async function () { | ||
83 | await makeGetRequest({ url, path, token, query: { target: 'bad target' } }) | ||
84 | }) | ||
85 | |||
86 | it('Should fail without target', async function () { | ||
87 | await makeGetRequest({ url, path, token }) | ||
88 | }) | ||
89 | |||
90 | it('Should succeed with the correct params', async function () { | ||
91 | await makeGetRequest({ url, path, token, query: { target: 'my-videos' }, expectedStatus: HttpStatusCode.OK_200 }) | ||
92 | }) | ||
93 | }) | ||
94 | |||
95 | describe('When manually adding a redundancy', function () { | ||
96 | const path = '/api/v1/server/redundancy/videos' | ||
97 | |||
98 | let url: string | ||
99 | let token: string | ||
100 | |||
101 | before(function () { | ||
102 | url = servers[0].url | ||
103 | token = servers[0].accessToken | ||
104 | }) | ||
105 | |||
106 | it('Should fail with an invalid token', async function () { | ||
107 | await makePostBodyRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
108 | }) | ||
109 | |||
110 | it('Should fail if the user is not an administrator', async function () { | ||
111 | await makePostBodyRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
112 | }) | ||
113 | |||
114 | it('Should fail without a video id', async function () { | ||
115 | await makePostBodyRequest({ url, path, token }) | ||
116 | }) | ||
117 | |||
118 | it('Should fail with an incorrect video id', async function () { | ||
119 | await makePostBodyRequest({ url, path, token, fields: { videoId: 'peertube' } }) | ||
120 | }) | ||
121 | |||
122 | it('Should fail with a not found video id', async function () { | ||
123 | await makePostBodyRequest({ url, path, token, fields: { videoId: 6565 }, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
124 | }) | ||
125 | |||
126 | it('Should fail with a local a video id', async function () { | ||
127 | await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdLocal } }) | ||
128 | }) | ||
129 | |||
130 | it('Should succeed with the correct params', async function () { | ||
131 | await makePostBodyRequest({ | ||
132 | url, | ||
133 | path, | ||
134 | token, | ||
135 | fields: { videoId: videoRemote.shortUUID }, | ||
136 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
137 | }) | ||
138 | }) | ||
139 | |||
140 | it('Should fail if the video is already duplicated', async function () { | ||
141 | this.timeout(30000) | ||
142 | |||
143 | await waitJobs(servers) | ||
144 | |||
145 | await makePostBodyRequest({ | ||
146 | url, | ||
147 | path, | ||
148 | token, | ||
149 | fields: { videoId: videoRemote.uuid }, | ||
150 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
151 | }) | ||
152 | }) | ||
153 | }) | ||
154 | |||
155 | describe('When manually removing a redundancy', function () { | ||
156 | const path = '/api/v1/server/redundancy/videos/' | ||
157 | |||
158 | let url: string | ||
159 | let token: string | ||
160 | |||
161 | before(function () { | ||
162 | url = servers[0].url | ||
163 | token = servers[0].accessToken | ||
164 | }) | ||
165 | |||
166 | it('Should fail with an invalid token', async function () { | ||
167 | await makeDeleteRequest({ url, path: path + '1', token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
168 | }) | ||
169 | |||
170 | it('Should fail if the user is not an administrator', async function () { | ||
171 | await makeDeleteRequest({ url, path: path + '1', token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
172 | }) | ||
173 | |||
174 | it('Should fail with an incorrect video id', async function () { | ||
175 | await makeDeleteRequest({ url, path: path + 'toto', token }) | ||
176 | }) | ||
177 | |||
178 | it('Should fail with a not found video redundancy', async function () { | ||
179 | await makeDeleteRequest({ url, path: path + '454545', token, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
180 | }) | ||
181 | }) | ||
182 | |||
183 | describe('When updating server redundancy', function () { | ||
184 | const path = '/api/v1/server/redundancy' | ||
185 | |||
186 | it('Should fail with an invalid token', async function () { | ||
187 | await makePutBodyRequest({ | ||
188 | url: servers[0].url, | ||
189 | path: path + '/' + servers[1].host, | ||
190 | fields: { redundancyAllowed: true }, | ||
191 | token: 'fake_token', | ||
192 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
193 | }) | ||
194 | }) | ||
195 | |||
196 | it('Should fail if the user is not an administrator', async function () { | ||
197 | await makePutBodyRequest({ | ||
198 | url: servers[0].url, | ||
199 | path: path + '/' + servers[1].host, | ||
200 | fields: { redundancyAllowed: true }, | ||
201 | token: userAccessToken, | ||
202 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
203 | }) | ||
204 | }) | ||
205 | |||
206 | it('Should fail if we do not follow this server', async function () { | ||
207 | await makePutBodyRequest({ | ||
208 | url: servers[0].url, | ||
209 | path: path + '/example.com', | ||
210 | fields: { redundancyAllowed: true }, | ||
211 | token: servers[0].accessToken, | ||
212 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
213 | }) | ||
214 | }) | ||
215 | |||
216 | it('Should fail without de redundancyAllowed param', async function () { | ||
217 | await makePutBodyRequest({ | ||
218 | url: servers[0].url, | ||
219 | path: path + '/' + servers[1].host, | ||
220 | fields: { blabla: true }, | ||
221 | token: servers[0].accessToken, | ||
222 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
223 | }) | ||
224 | }) | ||
225 | |||
226 | it('Should succeed with the correct parameters', async function () { | ||
227 | await makePutBodyRequest({ | ||
228 | url: servers[0].url, | ||
229 | path: path + '/' + servers[1].host, | ||
230 | fields: { redundancyAllowed: true }, | ||
231 | token: servers[0].accessToken, | ||
232 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
233 | }) | ||
234 | }) | ||
235 | }) | ||
236 | |||
237 | after(async function () { | ||
238 | await cleanupTests(servers) | ||
239 | }) | ||
240 | }) | ||
diff --git a/packages/tests/src/api/check-params/registrations.ts b/packages/tests/src/api/check-params/registrations.ts new file mode 100644 index 000000000..e4e46da2a --- /dev/null +++ b/packages/tests/src/api/check-params/registrations.ts | |||
@@ -0,0 +1,446 @@ | |||
1 | import { omit } from '@peertube/peertube-core-utils' | ||
2 | import { HttpStatusCode, HttpStatusCodeType, UserRole } from '@peertube/peertube-models' | ||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makePostBodyRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test registrations API validators', function () { | ||
15 | let server: PeerTubeServer | ||
16 | let userToken: string | ||
17 | let moderatorToken: string | ||
18 | |||
19 | // --------------------------------------------------------------- | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(30000) | ||
23 | |||
24 | server = await createSingleServer(1) | ||
25 | |||
26 | await setAccessTokensToServers([ server ]) | ||
27 | await setDefaultAccountAvatar([ server ]) | ||
28 | await setDefaultChannelAvatar([ server ]) | ||
29 | |||
30 | await server.config.enableSignup(false); | ||
31 | |||
32 | ({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR)); | ||
33 | ({ token: userToken } = await server.users.generate('user', UserRole.USER)) | ||
34 | }) | ||
35 | |||
36 | describe('Register', function () { | ||
37 | const registrationPath = '/api/v1/users/register' | ||
38 | const registrationRequestPath = '/api/v1/users/registrations/request' | ||
39 | |||
40 | const baseCorrectParams = { | ||
41 | username: 'user3', | ||
42 | displayName: 'super user', | ||
43 | email: 'test3@example.com', | ||
44 | password: 'my super password', | ||
45 | registrationReason: 'my super registration reason' | ||
46 | } | ||
47 | |||
48 | describe('When registering a new user or requesting user registration', function () { | ||
49 | |||
50 | async function check (fields: any, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) { | ||
51 | await server.config.enableSignup(false) | ||
52 | await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus }) | ||
53 | |||
54 | await server.config.enableSignup(true) | ||
55 | await makePostBodyRequest({ url: server.url, path: registrationRequestPath, fields, expectedStatus }) | ||
56 | } | ||
57 | |||
58 | it('Should fail with a too small username', async function () { | ||
59 | const fields = { ...baseCorrectParams, username: '' } | ||
60 | |||
61 | await check(fields) | ||
62 | }) | ||
63 | |||
64 | it('Should fail with a too long username', async function () { | ||
65 | const fields = { ...baseCorrectParams, username: 'super'.repeat(50) } | ||
66 | |||
67 | await check(fields) | ||
68 | }) | ||
69 | |||
70 | it('Should fail with an incorrect username', async function () { | ||
71 | const fields = { ...baseCorrectParams, username: 'my username' } | ||
72 | |||
73 | await check(fields) | ||
74 | }) | ||
75 | |||
76 | it('Should fail with a missing email', async function () { | ||
77 | const fields = omit(baseCorrectParams, [ 'email' ]) | ||
78 | |||
79 | await check(fields) | ||
80 | }) | ||
81 | |||
82 | it('Should fail with an invalid email', async function () { | ||
83 | const fields = { ...baseCorrectParams, email: 'test_example.com' } | ||
84 | |||
85 | await check(fields) | ||
86 | }) | ||
87 | |||
88 | it('Should fail with a too small password', async function () { | ||
89 | const fields = { ...baseCorrectParams, password: 'bla' } | ||
90 | |||
91 | await check(fields) | ||
92 | }) | ||
93 | |||
94 | it('Should fail with a too long password', async function () { | ||
95 | const fields = { ...baseCorrectParams, password: 'super'.repeat(61) } | ||
96 | |||
97 | await check(fields) | ||
98 | }) | ||
99 | |||
100 | it('Should fail if we register a user with the same username', async function () { | ||
101 | const fields = { ...baseCorrectParams, username: 'root' } | ||
102 | |||
103 | await check(fields, HttpStatusCode.CONFLICT_409) | ||
104 | }) | ||
105 | |||
106 | it('Should fail with a "peertube" username', async function () { | ||
107 | const fields = { ...baseCorrectParams, username: 'peertube' } | ||
108 | |||
109 | await check(fields, HttpStatusCode.CONFLICT_409) | ||
110 | }) | ||
111 | |||
112 | it('Should fail if we register a user with the same email', async function () { | ||
113 | const fields = { ...baseCorrectParams, email: 'admin' + server.internalServerNumber + '@example.com' } | ||
114 | |||
115 | await check(fields, HttpStatusCode.CONFLICT_409) | ||
116 | }) | ||
117 | |||
118 | it('Should fail with a bad display name', async function () { | ||
119 | const fields = { ...baseCorrectParams, displayName: 'a'.repeat(150) } | ||
120 | |||
121 | await check(fields) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with a bad channel name', async function () { | ||
125 | const fields = { ...baseCorrectParams, channel: { name: '[]azf', displayName: 'toto' } } | ||
126 | |||
127 | await check(fields) | ||
128 | }) | ||
129 | |||
130 | it('Should fail with a bad channel display name', async function () { | ||
131 | const fields = { ...baseCorrectParams, channel: { name: 'toto', displayName: '' } } | ||
132 | |||
133 | await check(fields) | ||
134 | }) | ||
135 | |||
136 | it('Should fail with a channel name that is the same as username', async function () { | ||
137 | const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } } | ||
138 | const fields = { ...baseCorrectParams, ...source } | ||
139 | |||
140 | await check(fields) | ||
141 | }) | ||
142 | |||
143 | it('Should fail with an existing channel', async function () { | ||
144 | const attributes = { name: 'existing_channel', displayName: 'hello', description: 'super description' } | ||
145 | await server.channels.create({ attributes }) | ||
146 | |||
147 | const fields = { ...baseCorrectParams, channel: { name: 'existing_channel', displayName: 'toto' } } | ||
148 | |||
149 | await check(fields, HttpStatusCode.CONFLICT_409) | ||
150 | }) | ||
151 | |||
152 | it('Should fail on a server with registration disabled', async function () { | ||
153 | this.timeout(60000) | ||
154 | |||
155 | await server.config.updateExistingSubConfig({ | ||
156 | newConfig: { | ||
157 | signup: { | ||
158 | enabled: false | ||
159 | } | ||
160 | } | ||
161 | }) | ||
162 | |||
163 | await server.registrations.register({ username: 'user4', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
164 | await server.registrations.requestRegistration({ | ||
165 | username: 'user4', | ||
166 | registrationReason: 'reason', | ||
167 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
168 | }) | ||
169 | }) | ||
170 | |||
171 | it('Should fail if the user limit is reached', async function () { | ||
172 | this.timeout(60000) | ||
173 | |||
174 | const { total } = await server.users.list() | ||
175 | |||
176 | await server.config.enableSignup(false, total) | ||
177 | await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
178 | |||
179 | await server.config.enableSignup(true, total) | ||
180 | await server.registrations.requestRegistration({ | ||
181 | username: 'user42', | ||
182 | registrationReason: 'reason', | ||
183 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
184 | }) | ||
185 | }) | ||
186 | |||
187 | it('Should succeed if the user limit is not reached', async function () { | ||
188 | this.timeout(60000) | ||
189 | |||
190 | const { total } = await server.users.list() | ||
191 | |||
192 | await server.config.enableSignup(false, total + 1) | ||
193 | await server.registrations.register({ username: 'user43', expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
194 | |||
195 | await server.config.enableSignup(true, total + 2) | ||
196 | await server.registrations.requestRegistration({ | ||
197 | username: 'user44', | ||
198 | registrationReason: 'reason', | ||
199 | expectedStatus: HttpStatusCode.OK_200 | ||
200 | }) | ||
201 | }) | ||
202 | }) | ||
203 | |||
204 | describe('On direct registration', function () { | ||
205 | |||
206 | it('Should succeed with the correct params', async function () { | ||
207 | await server.config.enableSignup(false) | ||
208 | |||
209 | const fields = { | ||
210 | username: 'user_direct_1', | ||
211 | displayName: 'super user direct 1', | ||
212 | email: 'user_direct_1@example.com', | ||
213 | password: 'my super password', | ||
214 | channel: { name: 'super_user_direct_1_channel', displayName: 'super user direct 1 channel' } | ||
215 | } | ||
216 | |||
217 | await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
218 | }) | ||
219 | |||
220 | it('Should fail if the instance requires approval', async function () { | ||
221 | this.timeout(60000) | ||
222 | |||
223 | await server.config.enableSignup(true) | ||
224 | await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
225 | }) | ||
226 | }) | ||
227 | |||
228 | describe('On registration request', function () { | ||
229 | |||
230 | before(async function () { | ||
231 | this.timeout(60000) | ||
232 | |||
233 | await server.config.enableSignup(true) | ||
234 | }) | ||
235 | |||
236 | it('Should fail with an invalid registration reason', async function () { | ||
237 | for (const registrationReason of [ '', 't', 't'.repeat(5000) ]) { | ||
238 | await server.registrations.requestRegistration({ | ||
239 | username: 'user_request_1', | ||
240 | registrationReason, | ||
241 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
242 | }) | ||
243 | } | ||
244 | }) | ||
245 | |||
246 | it('Should succeed with the correct params', async function () { | ||
247 | await server.registrations.requestRegistration({ | ||
248 | username: 'user_request_2', | ||
249 | registrationReason: 'tt', | ||
250 | channel: { | ||
251 | displayName: 'my user request 2 channel', | ||
252 | name: 'user_request_2_channel' | ||
253 | } | ||
254 | }) | ||
255 | }) | ||
256 | |||
257 | it('Should fail if the username is already awaiting registration approval', async function () { | ||
258 | await server.registrations.requestRegistration({ | ||
259 | username: 'user_request_2', | ||
260 | registrationReason: 'tt', | ||
261 | channel: { | ||
262 | displayName: 'my user request 42 channel', | ||
263 | name: 'user_request_42_channel' | ||
264 | }, | ||
265 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
266 | }) | ||
267 | }) | ||
268 | |||
269 | it('Should fail if the email is already awaiting registration approval', async function () { | ||
270 | await server.registrations.requestRegistration({ | ||
271 | username: 'user42', | ||
272 | email: 'user_request_2@example.com', | ||
273 | registrationReason: 'tt', | ||
274 | channel: { | ||
275 | displayName: 'my user request 42 channel', | ||
276 | name: 'user_request_42_channel' | ||
277 | }, | ||
278 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
279 | }) | ||
280 | }) | ||
281 | |||
282 | it('Should fail if the channel is already awaiting registration approval', async function () { | ||
283 | await server.registrations.requestRegistration({ | ||
284 | username: 'user42', | ||
285 | registrationReason: 'tt', | ||
286 | channel: { | ||
287 | displayName: 'my user request 2 channel', | ||
288 | name: 'user_request_2_channel' | ||
289 | }, | ||
290 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
291 | }) | ||
292 | }) | ||
293 | |||
294 | it('Should fail if the instance does not require approval', async function () { | ||
295 | this.timeout(60000) | ||
296 | |||
297 | await server.config.enableSignup(false) | ||
298 | |||
299 | await server.registrations.requestRegistration({ | ||
300 | username: 'user42', | ||
301 | registrationReason: 'toto', | ||
302 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
303 | }) | ||
304 | }) | ||
305 | }) | ||
306 | }) | ||
307 | |||
308 | describe('Registrations accept/reject', function () { | ||
309 | let id1: number | ||
310 | let id2: number | ||
311 | |||
312 | before(async function () { | ||
313 | this.timeout(60000) | ||
314 | |||
315 | await server.config.enableSignup(true); | ||
316 | |||
317 | ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_2', registrationReason: 'toto' })); | ||
318 | ({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' })) | ||
319 | }) | ||
320 | |||
321 | it('Should fail to accept/reject registration without token', async function () { | ||
322 | const options = { id: id1, moderationResponse: 'tt', token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 } | ||
323 | await server.registrations.accept(options) | ||
324 | await server.registrations.reject(options) | ||
325 | }) | ||
326 | |||
327 | it('Should fail to accept/reject registration with a non moderator user', async function () { | ||
328 | const options = { id: id1, moderationResponse: 'tt', token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 } | ||
329 | await server.registrations.accept(options) | ||
330 | await server.registrations.reject(options) | ||
331 | }) | ||
332 | |||
333 | it('Should fail to accept/reject registration with a bad registration id', async function () { | ||
334 | { | ||
335 | const options = { id: 't' as any, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
336 | await server.registrations.accept(options) | ||
337 | await server.registrations.reject(options) | ||
338 | } | ||
339 | |||
340 | { | ||
341 | const options = { id: 42, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
342 | await server.registrations.accept(options) | ||
343 | await server.registrations.reject(options) | ||
344 | } | ||
345 | }) | ||
346 | |||
347 | it('Should fail to accept/reject registration with a bad moderation resposne', async function () { | ||
348 | for (const moderationResponse of [ '', 't', 't'.repeat(5000) ]) { | ||
349 | const options = { id: id1, moderationResponse, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
350 | await server.registrations.accept(options) | ||
351 | await server.registrations.reject(options) | ||
352 | } | ||
353 | }) | ||
354 | |||
355 | it('Should succeed to accept a registration', async function () { | ||
356 | await server.registrations.accept({ id: id1, moderationResponse: 'tt', token: moderatorToken }) | ||
357 | }) | ||
358 | |||
359 | it('Should succeed to reject a registration', async function () { | ||
360 | await server.registrations.reject({ id: id2, moderationResponse: 'tt', token: moderatorToken }) | ||
361 | }) | ||
362 | |||
363 | it('Should fail to accept/reject a registration that was already accepted/rejected', async function () { | ||
364 | for (const id of [ id1, id2 ]) { | ||
365 | const options = { id, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.CONFLICT_409 } | ||
366 | await server.registrations.accept(options) | ||
367 | await server.registrations.reject(options) | ||
368 | } | ||
369 | }) | ||
370 | }) | ||
371 | |||
372 | describe('Registrations deletion', function () { | ||
373 | let id1: number | ||
374 | let id2: number | ||
375 | let id3: number | ||
376 | |||
377 | before(async function () { | ||
378 | ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_4', registrationReason: 'toto' })); | ||
379 | ({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_5', registrationReason: 'toto' })); | ||
380 | ({ id: id3 } = await server.registrations.requestRegistration({ username: 'request_6', registrationReason: 'toto' })) | ||
381 | |||
382 | await server.registrations.accept({ id: id2, moderationResponse: 'tt' }) | ||
383 | await server.registrations.reject({ id: id3, moderationResponse: 'tt' }) | ||
384 | }) | ||
385 | |||
386 | it('Should fail to delete registration without token', async function () { | ||
387 | await server.registrations.delete({ id: id1, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
388 | }) | ||
389 | |||
390 | it('Should fail to delete registration with a non moderator user', async function () { | ||
391 | await server.registrations.delete({ id: id1, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
392 | }) | ||
393 | |||
394 | it('Should fail to delete registration with a bad registration id', async function () { | ||
395 | await server.registrations.delete({ id: 't' as any, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
396 | await server.registrations.delete({ id: 42, token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
397 | }) | ||
398 | |||
399 | it('Should succeed with the correct params', async function () { | ||
400 | await server.registrations.delete({ id: id1, token: moderatorToken }) | ||
401 | await server.registrations.delete({ id: id2, token: moderatorToken }) | ||
402 | await server.registrations.delete({ id: id3, token: moderatorToken }) | ||
403 | }) | ||
404 | }) | ||
405 | |||
406 | describe('Listing registrations', function () { | ||
407 | const path = '/api/v1/users/registrations' | ||
408 | |||
409 | it('Should fail with a bad start pagination', async function () { | ||
410 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
411 | }) | ||
412 | |||
413 | it('Should fail with a bad count pagination', async function () { | ||
414 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
415 | }) | ||
416 | |||
417 | it('Should fail with an incorrect sort', async function () { | ||
418 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
419 | }) | ||
420 | |||
421 | it('Should fail with a non authenticated user', async function () { | ||
422 | await server.registrations.list({ | ||
423 | token: null, | ||
424 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
425 | }) | ||
426 | }) | ||
427 | |||
428 | it('Should fail with a non admin user', async function () { | ||
429 | await server.registrations.list({ | ||
430 | token: userToken, | ||
431 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
432 | }) | ||
433 | }) | ||
434 | |||
435 | it('Should succeed with the correct params', async function () { | ||
436 | await server.registrations.list({ | ||
437 | token: moderatorToken, | ||
438 | search: 'toto' | ||
439 | }) | ||
440 | }) | ||
441 | }) | ||
442 | |||
443 | after(async function () { | ||
444 | await cleanupTests([ server ]) | ||
445 | }) | ||
446 | }) | ||
diff --git a/packages/tests/src/api/check-params/runners.ts b/packages/tests/src/api/check-params/runners.ts new file mode 100644 index 000000000..dd2d2f0a1 --- /dev/null +++ b/packages/tests/src/api/check-params/runners.ts | |||
@@ -0,0 +1,911 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { basename } from 'path' | ||
3 | import { | ||
4 | HttpStatusCode, | ||
5 | HttpStatusCodeType, | ||
6 | isVideoStudioTaskIntro, | ||
7 | RunnerJob, | ||
8 | RunnerJobState, | ||
9 | RunnerJobStudioTranscodingPayload, | ||
10 | RunnerJobSuccessPayload, | ||
11 | RunnerJobUpdatePayload, | ||
12 | VideoPrivacy, | ||
13 | VideoStudioTaskIntro | ||
14 | } from '@peertube/peertube-models' | ||
15 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createSingleServer, | ||
19 | makePostBodyRequest, | ||
20 | PeerTubeServer, | ||
21 | sendRTMPStream, | ||
22 | setAccessTokensToServers, | ||
23 | setDefaultVideoChannel, | ||
24 | stopFfmpeg, | ||
25 | VideoStudioCommand, | ||
26 | waitJobs | ||
27 | } from '@peertube/peertube-server-commands' | ||
28 | |||
29 | const badUUID = '910ec12a-d9e6-458b-a274-0abb655f9464' | ||
30 | |||
31 | describe('Test managing runners', function () { | ||
32 | let server: PeerTubeServer | ||
33 | |||
34 | let userToken: string | ||
35 | |||
36 | let registrationTokenId: number | ||
37 | let registrationToken: string | ||
38 | |||
39 | let runnerToken: string | ||
40 | let runnerToken2: string | ||
41 | |||
42 | let completedJobToken: string | ||
43 | let completedJobUUID: string | ||
44 | |||
45 | let cancelledJobToken: string | ||
46 | let cancelledJobUUID: string | ||
47 | |||
48 | before(async function () { | ||
49 | this.timeout(120000) | ||
50 | |||
51 | const config = { | ||
52 | rates_limit: { | ||
53 | api: { | ||
54 | max: 5000 | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | |||
59 | server = await createSingleServer(1, config) | ||
60 | await setAccessTokensToServers([ server ]) | ||
61 | await setDefaultVideoChannel([ server ]) | ||
62 | |||
63 | userToken = await server.users.generateUserAndToken('user1') | ||
64 | |||
65 | const { data } = await server.runnerRegistrationTokens.list() | ||
66 | registrationToken = data[0].registrationToken | ||
67 | registrationTokenId = data[0].id | ||
68 | |||
69 | await server.config.enableTranscoding({ hls: true, webVideo: true }) | ||
70 | await server.config.enableStudio() | ||
71 | await server.config.enableRemoteTranscoding() | ||
72 | await server.config.enableRemoteStudio() | ||
73 | |||
74 | runnerToken = await server.runners.autoRegisterRunner() | ||
75 | runnerToken2 = await server.runners.autoRegisterRunner() | ||
76 | |||
77 | { | ||
78 | await server.videos.quickUpload({ name: 'video 1' }) | ||
79 | await server.videos.quickUpload({ name: 'video 2' }) | ||
80 | |||
81 | await waitJobs([ server ]) | ||
82 | |||
83 | { | ||
84 | const job = await server.runnerJobs.autoProcessWebVideoJob(runnerToken) | ||
85 | completedJobToken = job.jobToken | ||
86 | completedJobUUID = job.uuid | ||
87 | } | ||
88 | |||
89 | { | ||
90 | const { job } = await server.runnerJobs.autoAccept({ runnerToken }) | ||
91 | cancelledJobToken = job.jobToken | ||
92 | cancelledJobUUID = job.uuid | ||
93 | await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID }) | ||
94 | } | ||
95 | } | ||
96 | }) | ||
97 | |||
98 | describe('Managing runner registration tokens', function () { | ||
99 | |||
100 | describe('Common', function () { | ||
101 | |||
102 | it('Should fail to generate, list or delete runner registration token without oauth token', async function () { | ||
103 | const expectedStatus = HttpStatusCode.UNAUTHORIZED_401 | ||
104 | |||
105 | await server.runnerRegistrationTokens.generate({ token: null, expectedStatus }) | ||
106 | await server.runnerRegistrationTokens.list({ token: null, expectedStatus }) | ||
107 | await server.runnerRegistrationTokens.delete({ token: null, id: registrationTokenId, expectedStatus }) | ||
108 | }) | ||
109 | |||
110 | it('Should fail to generate, list or delete runner registration token without admin rights', async function () { | ||
111 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 | ||
112 | |||
113 | await server.runnerRegistrationTokens.generate({ token: userToken, expectedStatus }) | ||
114 | await server.runnerRegistrationTokens.list({ token: userToken, expectedStatus }) | ||
115 | await server.runnerRegistrationTokens.delete({ token: userToken, id: registrationTokenId, expectedStatus }) | ||
116 | }) | ||
117 | }) | ||
118 | |||
119 | describe('Delete', function () { | ||
120 | |||
121 | it('Should fail to delete with a bad id', async function () { | ||
122 | await server.runnerRegistrationTokens.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
123 | }) | ||
124 | }) | ||
125 | |||
126 | describe('List', function () { | ||
127 | const path = '/api/v1/runners/registration-tokens' | ||
128 | |||
129 | it('Should fail to list with a bad start pagination', async function () { | ||
130 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
131 | }) | ||
132 | |||
133 | it('Should fail to list with a bad count pagination', async function () { | ||
134 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
135 | }) | ||
136 | |||
137 | it('Should fail to list with an incorrect sort', async function () { | ||
138 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
139 | }) | ||
140 | |||
141 | it('Should succeed to list with the correct params', async function () { | ||
142 | await server.runnerRegistrationTokens.list({ start: 0, count: 5, sort: '-createdAt' }) | ||
143 | }) | ||
144 | }) | ||
145 | }) | ||
146 | |||
147 | describe('Managing runners', function () { | ||
148 | let toDeleteId: number | ||
149 | |||
150 | describe('Register', function () { | ||
151 | const name = 'runner name' | ||
152 | |||
153 | it('Should fail with a bad registration token', async function () { | ||
154 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
155 | |||
156 | await server.runners.register({ name, registrationToken: 'a'.repeat(4000), expectedStatus }) | ||
157 | await server.runners.register({ name, registrationToken: null, expectedStatus }) | ||
158 | }) | ||
159 | |||
160 | it('Should fail with an unknown registration token', async function () { | ||
161 | await server.runners.register({ name, registrationToken: 'aaa', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
162 | }) | ||
163 | |||
164 | it('Should fail with a bad name', async function () { | ||
165 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
166 | |||
167 | await server.runners.register({ name: '', registrationToken, expectedStatus }) | ||
168 | await server.runners.register({ name: 'a'.repeat(200), registrationToken, expectedStatus }) | ||
169 | }) | ||
170 | |||
171 | it('Should fail with an invalid description', async function () { | ||
172 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
173 | |||
174 | await server.runners.register({ name, description: '', registrationToken, expectedStatus }) | ||
175 | await server.runners.register({ name, description: 'a'.repeat(5000), registrationToken, expectedStatus }) | ||
176 | }) | ||
177 | |||
178 | it('Should succeed with the correct params', async function () { | ||
179 | const { id } = await server.runners.register({ name, description: 'super description', registrationToken }) | ||
180 | |||
181 | toDeleteId = id | ||
182 | }) | ||
183 | |||
184 | it('Should fail with the same runner name', async function () { | ||
185 | await server.runners.register({ | ||
186 | name, | ||
187 | description: 'super description', | ||
188 | registrationToken, | ||
189 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
190 | }) | ||
191 | }) | ||
192 | }) | ||
193 | |||
194 | describe('Delete', function () { | ||
195 | |||
196 | it('Should fail without oauth token', async function () { | ||
197 | await server.runners.delete({ token: null, id: toDeleteId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
198 | }) | ||
199 | |||
200 | it('Should fail without admin rights', async function () { | ||
201 | await server.runners.delete({ token: userToken, id: toDeleteId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
202 | }) | ||
203 | |||
204 | it('Should fail with a bad id', async function () { | ||
205 | await server.runners.delete({ id: 'hi' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
206 | }) | ||
207 | |||
208 | it('Should fail with an unknown id', async function () { | ||
209 | await server.runners.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
210 | }) | ||
211 | |||
212 | it('Should succeed with the correct params', async function () { | ||
213 | await server.runners.delete({ id: toDeleteId }) | ||
214 | }) | ||
215 | }) | ||
216 | |||
217 | describe('List', function () { | ||
218 | const path = '/api/v1/runners' | ||
219 | |||
220 | it('Should fail without oauth token', async function () { | ||
221 | await server.runners.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
222 | }) | ||
223 | |||
224 | it('Should fail without admin rights', async function () { | ||
225 | await server.runners.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
226 | }) | ||
227 | |||
228 | it('Should fail to list with a bad start pagination', async function () { | ||
229 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
230 | }) | ||
231 | |||
232 | it('Should fail to list with a bad count pagination', async function () { | ||
233 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
234 | }) | ||
235 | |||
236 | it('Should fail to list with an incorrect sort', async function () { | ||
237 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
238 | }) | ||
239 | |||
240 | it('Should fail with an invalid state', async function () { | ||
241 | await server.runners.list({ start: 0, count: 5, sort: '-createdAt' }) | ||
242 | }) | ||
243 | |||
244 | it('Should succeed to list with the correct params', async function () { | ||
245 | await server.runners.list({ start: 0, count: 5, sort: '-createdAt' }) | ||
246 | }) | ||
247 | }) | ||
248 | |||
249 | }) | ||
250 | |||
251 | describe('Runner jobs by admin', function () { | ||
252 | |||
253 | describe('Cancel', function () { | ||
254 | let jobUUID: string | ||
255 | |||
256 | before(async function () { | ||
257 | this.timeout(60000) | ||
258 | |||
259 | await server.videos.quickUpload({ name: 'video' }) | ||
260 | await waitJobs([ server ]) | ||
261 | |||
262 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
263 | jobUUID = availableJobs[0].uuid | ||
264 | }) | ||
265 | |||
266 | it('Should fail without oauth token', async function () { | ||
267 | await server.runnerJobs.cancelByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
268 | }) | ||
269 | |||
270 | it('Should fail without admin rights', async function () { | ||
271 | await server.runnerJobs.cancelByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
272 | }) | ||
273 | |||
274 | it('Should fail with a bad job uuid', async function () { | ||
275 | await server.runnerJobs.cancelByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
276 | }) | ||
277 | |||
278 | it('Should fail with an unknown job uuid', async function () { | ||
279 | const jobUUID = badUUID | ||
280 | await server.runnerJobs.cancelByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
281 | }) | ||
282 | |||
283 | it('Should fail with an already cancelled job', async function () { | ||
284 | await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
285 | }) | ||
286 | |||
287 | it('Should succeed with the correct params', async function () { | ||
288 | await server.runnerJobs.cancelByAdmin({ jobUUID }) | ||
289 | }) | ||
290 | }) | ||
291 | |||
292 | describe('List', function () { | ||
293 | const path = '/api/v1/runners/jobs' | ||
294 | |||
295 | it('Should fail without oauth token', async function () { | ||
296 | await server.runnerJobs.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
297 | }) | ||
298 | |||
299 | it('Should fail without admin rights', async function () { | ||
300 | await server.runnerJobs.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
301 | }) | ||
302 | |||
303 | it('Should fail to list with a bad start pagination', async function () { | ||
304 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
305 | }) | ||
306 | |||
307 | it('Should fail to list with a bad count pagination', async function () { | ||
308 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
309 | }) | ||
310 | |||
311 | it('Should fail to list with an incorrect sort', async function () { | ||
312 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
313 | }) | ||
314 | |||
315 | it('Should fail with an invalid state', async function () { | ||
316 | await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: 42 as any }) | ||
317 | await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ 42 ] as any }) | ||
318 | }) | ||
319 | |||
320 | it('Should succeed with the correct params', async function () { | ||
321 | await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ RunnerJobState.COMPLETED ] }) | ||
322 | }) | ||
323 | }) | ||
324 | |||
325 | describe('Delete', function () { | ||
326 | let jobUUID: string | ||
327 | |||
328 | before(async function () { | ||
329 | this.timeout(60000) | ||
330 | |||
331 | await server.videos.quickUpload({ name: 'video' }) | ||
332 | await waitJobs([ server ]) | ||
333 | |||
334 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
335 | jobUUID = availableJobs[0].uuid | ||
336 | }) | ||
337 | |||
338 | it('Should fail without oauth token', async function () { | ||
339 | await server.runnerJobs.deleteByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
340 | }) | ||
341 | |||
342 | it('Should fail without admin rights', async function () { | ||
343 | await server.runnerJobs.deleteByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
344 | }) | ||
345 | |||
346 | it('Should fail with a bad job uuid', async function () { | ||
347 | await server.runnerJobs.deleteByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
348 | }) | ||
349 | |||
350 | it('Should fail with an unknown job uuid', async function () { | ||
351 | const jobUUID = badUUID | ||
352 | await server.runnerJobs.deleteByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
353 | }) | ||
354 | |||
355 | it('Should succeed with the correct params', async function () { | ||
356 | await server.runnerJobs.deleteByAdmin({ jobUUID }) | ||
357 | }) | ||
358 | }) | ||
359 | |||
360 | }) | ||
361 | |||
362 | describe('Runner jobs by runners', function () { | ||
363 | let jobUUID: string | ||
364 | let jobToken: string | ||
365 | let videoUUID: string | ||
366 | |||
367 | let jobUUID2: string | ||
368 | let jobToken2: string | ||
369 | |||
370 | let videoUUID2: string | ||
371 | |||
372 | let pendingUUID: string | ||
373 | |||
374 | let videoStudioUUID: string | ||
375 | let studioFile: string | ||
376 | |||
377 | let liveAcceptedJob: RunnerJob & { jobToken: string } | ||
378 | let studioAcceptedJob: RunnerJob & { jobToken: string } | ||
379 | |||
380 | async function fetchVideoInputFiles (options: { | ||
381 | jobUUID: string | ||
382 | videoUUID: string | ||
383 | runnerToken: string | ||
384 | jobToken: string | ||
385 | expectedStatus: HttpStatusCodeType | ||
386 | }) { | ||
387 | const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken } = options | ||
388 | |||
389 | const basePath = '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID | ||
390 | const paths = [ `${basePath}/max-quality`, `${basePath}/previews/max-quality` ] | ||
391 | |||
392 | for (const path of paths) { | ||
393 | await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus }) | ||
394 | } | ||
395 | } | ||
396 | |||
397 | async function fetchStudioFiles (options: { | ||
398 | jobUUID: string | ||
399 | videoUUID: string | ||
400 | runnerToken: string | ||
401 | jobToken: string | ||
402 | studioFile?: string | ||
403 | expectedStatus: HttpStatusCodeType | ||
404 | }) { | ||
405 | const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken, studioFile } = options | ||
406 | |||
407 | const path = `/api/v1/runners/jobs/${jobUUID}/files/videos/${videoUUID}/studio/task-files/${studioFile}` | ||
408 | |||
409 | await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus }) | ||
410 | } | ||
411 | |||
412 | before(async function () { | ||
413 | this.timeout(120000) | ||
414 | |||
415 | { | ||
416 | await server.runnerJobs.cancelAllJobs({ state: RunnerJobState.PENDING }) | ||
417 | } | ||
418 | |||
419 | { | ||
420 | const { uuid } = await server.videos.quickUpload({ name: 'video' }) | ||
421 | videoUUID = uuid | ||
422 | |||
423 | await waitJobs([ server ]) | ||
424 | |||
425 | const { job } = await server.runnerJobs.autoAccept({ runnerToken }) | ||
426 | jobUUID = job.uuid | ||
427 | jobToken = job.jobToken | ||
428 | } | ||
429 | |||
430 | { | ||
431 | const { uuid } = await server.videos.quickUpload({ name: 'video' }) | ||
432 | videoUUID2 = uuid | ||
433 | |||
434 | await waitJobs([ server ]) | ||
435 | |||
436 | const { job } = await server.runnerJobs.autoAccept({ runnerToken: runnerToken2 }) | ||
437 | jobUUID2 = job.uuid | ||
438 | jobToken2 = job.jobToken | ||
439 | } | ||
440 | |||
441 | { | ||
442 | await server.videos.quickUpload({ name: 'video' }) | ||
443 | await waitJobs([ server ]) | ||
444 | |||
445 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
446 | pendingUUID = availableJobs[0].uuid | ||
447 | } | ||
448 | |||
449 | { | ||
450 | await server.config.disableTranscoding() | ||
451 | |||
452 | const { uuid } = await server.videos.quickUpload({ name: 'video studio' }) | ||
453 | videoStudioUUID = uuid | ||
454 | |||
455 | await server.config.enableTranscoding({ hls: true, webVideo: true }) | ||
456 | await server.config.enableStudio() | ||
457 | |||
458 | await server.videoStudio.createEditionTasks({ | ||
459 | videoId: videoStudioUUID, | ||
460 | tasks: VideoStudioCommand.getComplexTask() | ||
461 | }) | ||
462 | |||
463 | const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'video-studio-transcoding' }) | ||
464 | studioAcceptedJob = job | ||
465 | |||
466 | const tasks = (job.payload as RunnerJobStudioTranscodingPayload).tasks | ||
467 | const fileUrl = (tasks.find(t => isVideoStudioTaskIntro(t)) as VideoStudioTaskIntro).options.file as string | ||
468 | studioFile = basename(fileUrl) | ||
469 | } | ||
470 | |||
471 | { | ||
472 | await server.config.enableLive({ | ||
473 | allowReplay: false, | ||
474 | resolutions: 'max', | ||
475 | transcoding: true | ||
476 | }) | ||
477 | |||
478 | const { live } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) | ||
479 | |||
480 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
481 | await waitJobs([ server ]) | ||
482 | |||
483 | await server.runnerJobs.requestLiveJob(runnerToken) | ||
484 | |||
485 | const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' }) | ||
486 | liveAcceptedJob = job | ||
487 | |||
488 | await stopFfmpeg(ffmpegCommand) | ||
489 | } | ||
490 | }) | ||
491 | |||
492 | describe('Common runner tokens validations', function () { | ||
493 | |||
494 | async function testEndpoints (options: { | ||
495 | jobUUID: string | ||
496 | runnerToken: string | ||
497 | jobToken: string | ||
498 | expectedStatus: HttpStatusCodeType | ||
499 | }) { | ||
500 | await server.runnerJobs.abort({ ...options, reason: 'reason' }) | ||
501 | await server.runnerJobs.update({ ...options }) | ||
502 | await server.runnerJobs.error({ ...options, message: 'message' }) | ||
503 | await server.runnerJobs.success({ ...options, payload: { videoFile: 'video_short.mp4' } }) | ||
504 | } | ||
505 | |||
506 | it('Should fail with an invalid job uuid', async function () { | ||
507 | const options = { jobUUID: 'a', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
508 | |||
509 | await testEndpoints({ ...options, jobToken }) | ||
510 | await fetchVideoInputFiles({ ...options, videoUUID, jobToken }) | ||
511 | await fetchStudioFiles({ ...options, videoUUID, jobToken: studioAcceptedJob.jobToken, studioFile }) | ||
512 | }) | ||
513 | |||
514 | it('Should fail with an unknown job uuid', async function () { | ||
515 | const options = { jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
516 | |||
517 | await testEndpoints({ ...options, jobToken }) | ||
518 | await fetchVideoInputFiles({ ...options, videoUUID, jobToken }) | ||
519 | await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID, studioFile }) | ||
520 | }) | ||
521 | |||
522 | it('Should fail with an invalid runner token', async function () { | ||
523 | const options = { runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
524 | |||
525 | await testEndpoints({ ...options, jobUUID, jobToken }) | ||
526 | await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken }) | ||
527 | await fetchStudioFiles({ | ||
528 | ...options, | ||
529 | jobToken: studioAcceptedJob.jobToken, | ||
530 | jobUUID: studioAcceptedJob.uuid, | ||
531 | videoUUID: videoStudioUUID, | ||
532 | studioFile | ||
533 | }) | ||
534 | }) | ||
535 | |||
536 | it('Should fail with an unknown runner token', async function () { | ||
537 | const options = { runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
538 | |||
539 | await testEndpoints({ ...options, jobUUID, jobToken }) | ||
540 | await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken }) | ||
541 | await fetchStudioFiles({ | ||
542 | ...options, | ||
543 | jobToken: studioAcceptedJob.jobToken, | ||
544 | jobUUID: studioAcceptedJob.uuid, | ||
545 | videoUUID: videoStudioUUID, | ||
546 | studioFile | ||
547 | }) | ||
548 | }) | ||
549 | |||
550 | it('Should fail with an invalid job token job uuid', async function () { | ||
551 | const options = { runnerToken, jobToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 } | ||
552 | |||
553 | await testEndpoints({ ...options, jobUUID }) | ||
554 | await fetchVideoInputFiles({ ...options, jobUUID, videoUUID }) | ||
555 | await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile }) | ||
556 | }) | ||
557 | |||
558 | it('Should fail with an unknown job token job uuid', async function () { | ||
559 | const options = { runnerToken, jobToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
560 | |||
561 | await testEndpoints({ ...options, jobUUID }) | ||
562 | await fetchVideoInputFiles({ ...options, jobUUID, videoUUID }) | ||
563 | await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile }) | ||
564 | }) | ||
565 | |||
566 | it('Should fail with a runner token not associated to this job', async function () { | ||
567 | const options = { runnerToken: runnerToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
568 | |||
569 | await testEndpoints({ ...options, jobUUID, jobToken }) | ||
570 | await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken }) | ||
571 | await fetchStudioFiles({ | ||
572 | ...options, | ||
573 | jobToken: studioAcceptedJob.jobToken, | ||
574 | jobUUID: studioAcceptedJob.uuid, | ||
575 | videoUUID: videoStudioUUID, | ||
576 | studioFile | ||
577 | }) | ||
578 | }) | ||
579 | |||
580 | it('Should fail with a job uuid not associated to the job token', async function () { | ||
581 | { | ||
582 | const options = { jobUUID: jobUUID2, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
583 | |||
584 | await testEndpoints({ ...options, jobToken }) | ||
585 | await fetchVideoInputFiles({ ...options, jobToken, videoUUID }) | ||
586 | await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID: videoStudioUUID, studioFile }) | ||
587 | } | ||
588 | |||
589 | { | ||
590 | const options = { runnerToken, jobToken: jobToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 } | ||
591 | |||
592 | await testEndpoints({ ...options, jobUUID }) | ||
593 | await fetchVideoInputFiles({ ...options, jobUUID, videoUUID }) | ||
594 | await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile }) | ||
595 | } | ||
596 | }) | ||
597 | }) | ||
598 | |||
599 | describe('Unregister', function () { | ||
600 | |||
601 | it('Should fail without a runner token', async function () { | ||
602 | await server.runners.unregister({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
603 | }) | ||
604 | |||
605 | it('Should fail with a bad a runner token', async function () { | ||
606 | await server.runners.unregister({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
607 | }) | ||
608 | |||
609 | it('Should fail with an unknown runner token', async function () { | ||
610 | await server.runners.unregister({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
611 | }) | ||
612 | }) | ||
613 | |||
614 | describe('Request', function () { | ||
615 | |||
616 | it('Should fail without a runner token', async function () { | ||
617 | await server.runnerJobs.request({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
618 | }) | ||
619 | |||
620 | it('Should fail with a bad a runner token', async function () { | ||
621 | await server.runnerJobs.request({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
622 | }) | ||
623 | |||
624 | it('Should fail with an unknown runner token', async function () { | ||
625 | await server.runnerJobs.request({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
626 | }) | ||
627 | }) | ||
628 | |||
629 | describe('Accept', function () { | ||
630 | |||
631 | it('Should fail with a bad a job uuid', async function () { | ||
632 | await server.runnerJobs.accept({ jobUUID: '', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
633 | }) | ||
634 | |||
635 | it('Should fail with an unknown job uuid', async function () { | ||
636 | await server.runnerJobs.accept({ jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
637 | }) | ||
638 | |||
639 | it('Should fail with a job not in pending state', async function () { | ||
640 | await server.runnerJobs.accept({ jobUUID: completedJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
641 | await server.runnerJobs.accept({ jobUUID: cancelledJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
642 | }) | ||
643 | |||
644 | it('Should fail without a runner token', async function () { | ||
645 | await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
646 | }) | ||
647 | |||
648 | it('Should fail with a bad a runner token', async function () { | ||
649 | await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
650 | }) | ||
651 | |||
652 | it('Should fail with an unknown runner token', async function () { | ||
653 | await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
654 | }) | ||
655 | }) | ||
656 | |||
657 | describe('Abort', function () { | ||
658 | |||
659 | it('Should fail without a reason', async function () { | ||
660 | await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
661 | }) | ||
662 | |||
663 | it('Should fail with a bad reason', async function () { | ||
664 | const reason = 'reason'.repeat(5000) | ||
665 | await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
666 | }) | ||
667 | |||
668 | it('Should fail with a job not in processing state', async function () { | ||
669 | await server.runnerJobs.abort({ | ||
670 | jobUUID: completedJobUUID, | ||
671 | jobToken: completedJobToken, | ||
672 | runnerToken, | ||
673 | reason: 'reason', | ||
674 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
675 | }) | ||
676 | }) | ||
677 | }) | ||
678 | |||
679 | describe('Update', function () { | ||
680 | |||
681 | describe('Common', function () { | ||
682 | |||
683 | it('Should fail with an invalid progress', async function () { | ||
684 | await server.runnerJobs.update({ jobUUID, jobToken, runnerToken, progress: 101, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
685 | }) | ||
686 | |||
687 | it('Should fail with a job not in processing state', async function () { | ||
688 | await server.runnerJobs.update({ | ||
689 | jobUUID: cancelledJobUUID, | ||
690 | jobToken: cancelledJobToken, | ||
691 | runnerToken, | ||
692 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
693 | }) | ||
694 | }) | ||
695 | }) | ||
696 | |||
697 | describe('Live RTMP to HLS', function () { | ||
698 | const base: RunnerJobUpdatePayload = { | ||
699 | masterPlaylistFile: 'live/master.m3u8', | ||
700 | resolutionPlaylistFilename: '0.m3u8', | ||
701 | resolutionPlaylistFile: 'live/1.m3u8', | ||
702 | type: 'add-chunk', | ||
703 | videoChunkFile: 'live/1-000069.ts', | ||
704 | videoChunkFilename: '1-000068.ts' | ||
705 | } | ||
706 | |||
707 | function testUpdate (payload: RunnerJobUpdatePayload) { | ||
708 | return server.runnerJobs.update({ | ||
709 | jobUUID: liveAcceptedJob.uuid, | ||
710 | jobToken: liveAcceptedJob.jobToken, | ||
711 | payload, | ||
712 | runnerToken, | ||
713 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
714 | }) | ||
715 | } | ||
716 | |||
717 | it('Should fail with an invalid resolutionPlaylistFilename', async function () { | ||
718 | await testUpdate({ ...base, resolutionPlaylistFilename: undefined }) | ||
719 | await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' }) | ||
720 | await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' }) | ||
721 | }) | ||
722 | |||
723 | it('Should fail with an invalid videoChunkFilename', async function () { | ||
724 | await testUpdate({ ...base, resolutionPlaylistFilename: undefined }) | ||
725 | await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' }) | ||
726 | await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' }) | ||
727 | }) | ||
728 | |||
729 | it('Should fail with an invalid type', async function () { | ||
730 | await testUpdate({ ...base, type: undefined }) | ||
731 | await testUpdate({ ...base, type: 'toto' as any }) | ||
732 | }) | ||
733 | }) | ||
734 | }) | ||
735 | |||
736 | describe('Error', function () { | ||
737 | |||
738 | it('Should fail with a missing error message', async function () { | ||
739 | await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
740 | }) | ||
741 | |||
742 | it('Should fail with an invalid error messgae', async function () { | ||
743 | const message = 'a'.repeat(6000) | ||
744 | await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
745 | }) | ||
746 | |||
747 | it('Should fail with a job not in processing state', async function () { | ||
748 | await server.runnerJobs.error({ | ||
749 | jobUUID: completedJobUUID, | ||
750 | jobToken: completedJobToken, | ||
751 | message: 'my message', | ||
752 | runnerToken, | ||
753 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
754 | }) | ||
755 | }) | ||
756 | }) | ||
757 | |||
758 | describe('Success', function () { | ||
759 | let vodJobUUID: string | ||
760 | let vodJobToken: string | ||
761 | |||
762 | describe('Common', function () { | ||
763 | |||
764 | it('Should fail with a job not in processing state', async function () { | ||
765 | await server.runnerJobs.success({ | ||
766 | jobUUID: completedJobUUID, | ||
767 | jobToken: completedJobToken, | ||
768 | payload: { videoFile: 'video_short.mp4' }, | ||
769 | runnerToken, | ||
770 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
771 | }) | ||
772 | }) | ||
773 | }) | ||
774 | |||
775 | describe('VOD', function () { | ||
776 | |||
777 | it('Should fail with an invalid vod web video payload', async function () { | ||
778 | const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-web-video-transcoding' }) | ||
779 | |||
780 | await server.runnerJobs.success({ | ||
781 | jobUUID: job.uuid, | ||
782 | jobToken: job.jobToken, | ||
783 | payload: { hello: 'video_short.mp4' } as any, | ||
784 | runnerToken, | ||
785 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
786 | }) | ||
787 | |||
788 | vodJobUUID = job.uuid | ||
789 | vodJobToken = job.jobToken | ||
790 | }) | ||
791 | |||
792 | it('Should fail with an invalid vod hls payload', async function () { | ||
793 | // To create HLS jobs | ||
794 | const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' } | ||
795 | await server.runnerJobs.success({ runnerToken, jobUUID: vodJobUUID, jobToken: vodJobToken, payload }) | ||
796 | |||
797 | await waitJobs([ server ]) | ||
798 | |||
799 | const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-hls-transcoding' }) | ||
800 | |||
801 | await server.runnerJobs.success({ | ||
802 | jobUUID: job.uuid, | ||
803 | jobToken: job.jobToken, | ||
804 | payload: { videoFile: 'video_short.mp4' } as any, | ||
805 | runnerToken, | ||
806 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
807 | }) | ||
808 | }) | ||
809 | |||
810 | it('Should fail with an invalid vod audio merge payload', async function () { | ||
811 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } | ||
812 | await server.videos.upload({ attributes, mode: 'legacy' }) | ||
813 | |||
814 | await waitJobs([ server ]) | ||
815 | |||
816 | const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-audio-merge-transcoding' }) | ||
817 | |||
818 | await server.runnerJobs.success({ | ||
819 | jobUUID: job.uuid, | ||
820 | jobToken: job.jobToken, | ||
821 | payload: { hello: 'video_short.mp4' } as any, | ||
822 | runnerToken, | ||
823 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
824 | }) | ||
825 | }) | ||
826 | }) | ||
827 | |||
828 | describe('Video studio', function () { | ||
829 | |||
830 | it('Should fail with an invalid video studio transcoding payload', async function () { | ||
831 | await server.runnerJobs.success({ | ||
832 | jobUUID: studioAcceptedJob.uuid, | ||
833 | jobToken: studioAcceptedJob.jobToken, | ||
834 | payload: { hello: 'video_short.mp4' } as any, | ||
835 | runnerToken, | ||
836 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
837 | }) | ||
838 | }) | ||
839 | }) | ||
840 | }) | ||
841 | |||
842 | describe('Job files', function () { | ||
843 | |||
844 | describe('Check video param for common job file routes', function () { | ||
845 | |||
846 | async function fetchFiles (options: { | ||
847 | videoUUID?: string | ||
848 | expectedStatus: HttpStatusCodeType | ||
849 | }) { | ||
850 | await fetchVideoInputFiles({ videoUUID, ...options, jobToken, jobUUID, runnerToken }) | ||
851 | |||
852 | await fetchStudioFiles({ | ||
853 | videoUUID: videoStudioUUID, | ||
854 | |||
855 | ...options, | ||
856 | |||
857 | jobToken: studioAcceptedJob.jobToken, | ||
858 | jobUUID: studioAcceptedJob.uuid, | ||
859 | runnerToken, | ||
860 | studioFile | ||
861 | }) | ||
862 | } | ||
863 | |||
864 | it('Should fail with an invalid video id', async function () { | ||
865 | await fetchFiles({ | ||
866 | videoUUID: 'a', | ||
867 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
868 | }) | ||
869 | }) | ||
870 | |||
871 | it('Should fail with an unknown video id', async function () { | ||
872 | const videoUUID = '910ec12a-d9e6-458b-a274-0abb655f9464' | ||
873 | |||
874 | await fetchFiles({ | ||
875 | videoUUID, | ||
876 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
877 | }) | ||
878 | }) | ||
879 | |||
880 | it('Should fail with a video id not associated to this job', async function () { | ||
881 | await fetchFiles({ | ||
882 | videoUUID: videoUUID2, | ||
883 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
884 | }) | ||
885 | }) | ||
886 | |||
887 | it('Should succeed with the correct params', async function () { | ||
888 | await fetchFiles({ expectedStatus: HttpStatusCode.OK_200 }) | ||
889 | }) | ||
890 | }) | ||
891 | |||
892 | describe('Video studio tasks file routes', function () { | ||
893 | |||
894 | it('Should fail with an invalid studio filename', async function () { | ||
895 | await fetchStudioFiles({ | ||
896 | videoUUID: videoStudioUUID, | ||
897 | jobUUID: studioAcceptedJob.uuid, | ||
898 | runnerToken, | ||
899 | jobToken: studioAcceptedJob.jobToken, | ||
900 | studioFile: 'toto', | ||
901 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
902 | }) | ||
903 | }) | ||
904 | }) | ||
905 | }) | ||
906 | }) | ||
907 | |||
908 | after(async function () { | ||
909 | await cleanupTests([ server ]) | ||
910 | }) | ||
911 | }) | ||
diff --git a/packages/tests/src/api/check-params/search.ts b/packages/tests/src/api/check-params/search.ts new file mode 100644 index 000000000..b886cbc82 --- /dev/null +++ b/packages/tests/src/api/check-params/search.ts | |||
@@ -0,0 +1,278 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeGetRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | function updateSearchIndex (server: PeerTubeServer, enabled: boolean, disableLocalSearch = false) { | ||
14 | return server.config.updateCustomSubConfig({ | ||
15 | newConfig: { | ||
16 | search: { | ||
17 | searchIndex: { | ||
18 | enabled, | ||
19 | disableLocalSearch | ||
20 | } | ||
21 | } | ||
22 | } | ||
23 | }) | ||
24 | } | ||
25 | |||
26 | describe('Test videos API validator', function () { | ||
27 | let server: PeerTubeServer | ||
28 | |||
29 | // --------------------------------------------------------------- | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(30000) | ||
33 | |||
34 | server = await createSingleServer(1) | ||
35 | await setAccessTokensToServers([ server ]) | ||
36 | }) | ||
37 | |||
38 | describe('When searching videos', function () { | ||
39 | const path = '/api/v1/search/videos/' | ||
40 | |||
41 | const query = { | ||
42 | search: 'coucou' | ||
43 | } | ||
44 | |||
45 | it('Should fail with a bad start pagination', async function () { | ||
46 | await checkBadStartPagination(server.url, path, null, query) | ||
47 | }) | ||
48 | |||
49 | it('Should fail with a bad count pagination', async function () { | ||
50 | await checkBadCountPagination(server.url, path, null, query) | ||
51 | }) | ||
52 | |||
53 | it('Should fail with an incorrect sort', async function () { | ||
54 | await checkBadSortPagination(server.url, path, null, query) | ||
55 | }) | ||
56 | |||
57 | it('Should succeed with the correct parameters', async function () { | ||
58 | await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
59 | }) | ||
60 | |||
61 | it('Should fail with an invalid category', async function () { | ||
62 | const customQuery1 = { ...query, categoryOneOf: [ 'aa', 'b' ] } | ||
63 | await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
64 | |||
65 | const customQuery2 = { ...query, categoryOneOf: 'a' } | ||
66 | await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
67 | }) | ||
68 | |||
69 | it('Should succeed with a valid category', async function () { | ||
70 | const customQuery1 = { ...query, categoryOneOf: [ 1, 7 ] } | ||
71 | await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 }) | ||
72 | |||
73 | const customQuery2 = { ...query, categoryOneOf: 1 } | ||
74 | await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 }) | ||
75 | }) | ||
76 | |||
77 | it('Should fail with an invalid licence', async function () { | ||
78 | const customQuery1 = { ...query, licenceOneOf: [ 'aa', 'b' ] } | ||
79 | await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
80 | |||
81 | const customQuery2 = { ...query, licenceOneOf: 'a' } | ||
82 | await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
83 | }) | ||
84 | |||
85 | it('Should succeed with a valid licence', async function () { | ||
86 | const customQuery1 = { ...query, licenceOneOf: [ 1, 2 ] } | ||
87 | await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 }) | ||
88 | |||
89 | const customQuery2 = { ...query, licenceOneOf: 1 } | ||
90 | await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 }) | ||
91 | }) | ||
92 | |||
93 | it('Should succeed with a valid language', async function () { | ||
94 | const customQuery1 = { ...query, languageOneOf: [ 'fr', 'en' ] } | ||
95 | await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 }) | ||
96 | |||
97 | const customQuery2 = { ...query, languageOneOf: 'fr' } | ||
98 | await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 }) | ||
99 | }) | ||
100 | |||
101 | it('Should succeed with valid tags', async function () { | ||
102 | const customQuery1 = { ...query, tagsOneOf: [ 'tag1', 'tag2' ] } | ||
103 | await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 }) | ||
104 | |||
105 | const customQuery2 = { ...query, tagsOneOf: 'tag1' } | ||
106 | await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 }) | ||
107 | |||
108 | const customQuery3 = { ...query, tagsAllOf: [ 'tag1', 'tag2' ] } | ||
109 | await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.OK_200 }) | ||
110 | |||
111 | const customQuery4 = { ...query, tagsAllOf: 'tag1' } | ||
112 | await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.OK_200 }) | ||
113 | }) | ||
114 | |||
115 | it('Should fail with invalid durations', async function () { | ||
116 | const customQuery1 = { ...query, durationMin: 'hello' } | ||
117 | await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
118 | |||
119 | const customQuery2 = { ...query, durationMax: 'hello' } | ||
120 | await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
121 | }) | ||
122 | |||
123 | it('Should fail with invalid dates', async function () { | ||
124 | const customQuery1 = { ...query, startDate: 'hello' } | ||
125 | await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
126 | |||
127 | const customQuery2 = { ...query, endDate: 'hello' } | ||
128 | await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
129 | |||
130 | const customQuery3 = { ...query, originallyPublishedStartDate: 'hello' } | ||
131 | await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
132 | |||
133 | const customQuery4 = { ...query, originallyPublishedEndDate: 'hello' } | ||
134 | await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
135 | }) | ||
136 | |||
137 | it('Should fail with an invalid host', async function () { | ||
138 | const customQuery = { ...query, host: '6565' } | ||
139 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
140 | }) | ||
141 | |||
142 | it('Should succeed with a host', async function () { | ||
143 | const customQuery = { ...query, host: 'example.com' } | ||
144 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) | ||
145 | }) | ||
146 | |||
147 | it('Should fail with invalid uuids', async function () { | ||
148 | const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } | ||
149 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
150 | }) | ||
151 | |||
152 | it('Should succeed with valid uuids', async function () { | ||
153 | const customQuery = { ...query, uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } | ||
154 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) | ||
155 | }) | ||
156 | }) | ||
157 | |||
158 | describe('When searching video playlists', function () { | ||
159 | const path = '/api/v1/search/video-playlists/' | ||
160 | |||
161 | const query = { | ||
162 | search: 'coucou', | ||
163 | host: 'example.com' | ||
164 | } | ||
165 | |||
166 | it('Should fail with a bad start pagination', async function () { | ||
167 | await checkBadStartPagination(server.url, path, null, query) | ||
168 | }) | ||
169 | |||
170 | it('Should fail with a bad count pagination', async function () { | ||
171 | await checkBadCountPagination(server.url, path, null, query) | ||
172 | }) | ||
173 | |||
174 | it('Should fail with an incorrect sort', async function () { | ||
175 | await checkBadSortPagination(server.url, path, null, query) | ||
176 | }) | ||
177 | |||
178 | it('Should fail with an invalid host', async function () { | ||
179 | await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
180 | }) | ||
181 | |||
182 | it('Should fail with invalid uuids', async function () { | ||
183 | const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } | ||
184 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
185 | }) | ||
186 | |||
187 | it('Should succeed with the correct parameters', async function () { | ||
188 | await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
189 | }) | ||
190 | }) | ||
191 | |||
192 | describe('When searching video channels', function () { | ||
193 | const path = '/api/v1/search/video-channels/' | ||
194 | |||
195 | const query = { | ||
196 | search: 'coucou', | ||
197 | host: 'example.com' | ||
198 | } | ||
199 | |||
200 | it('Should fail with a bad start pagination', async function () { | ||
201 | await checkBadStartPagination(server.url, path, null, query) | ||
202 | }) | ||
203 | |||
204 | it('Should fail with a bad count pagination', async function () { | ||
205 | await checkBadCountPagination(server.url, path, null, query) | ||
206 | }) | ||
207 | |||
208 | it('Should fail with an incorrect sort', async function () { | ||
209 | await checkBadSortPagination(server.url, path, null, query) | ||
210 | }) | ||
211 | |||
212 | it('Should fail with an invalid host', async function () { | ||
213 | await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
214 | }) | ||
215 | |||
216 | it('Should fail with invalid handles', async function () { | ||
217 | await makeGetRequest({ url: server.url, path, query: { ...query, handles: [ '' ] }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
218 | }) | ||
219 | |||
220 | it('Should succeed with the correct parameters', async function () { | ||
221 | await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
222 | }) | ||
223 | }) | ||
224 | |||
225 | describe('Search target', function () { | ||
226 | |||
227 | it('Should fail/succeed depending on the search target', async function () { | ||
228 | const query = { search: 'coucou' } | ||
229 | const paths = [ | ||
230 | '/api/v1/search/video-playlists/', | ||
231 | '/api/v1/search/video-channels/', | ||
232 | '/api/v1/search/videos/' | ||
233 | ] | ||
234 | |||
235 | for (const path of paths) { | ||
236 | { | ||
237 | const customQuery = { ...query, searchTarget: 'hello' } | ||
238 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
239 | } | ||
240 | |||
241 | { | ||
242 | const customQuery = { ...query, searchTarget: undefined } | ||
243 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) | ||
244 | } | ||
245 | |||
246 | { | ||
247 | const customQuery = { ...query, searchTarget: 'local' } | ||
248 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) | ||
249 | } | ||
250 | |||
251 | { | ||
252 | const customQuery = { ...query, searchTarget: 'search-index' } | ||
253 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
254 | } | ||
255 | |||
256 | await updateSearchIndex(server, true, true) | ||
257 | |||
258 | { | ||
259 | const customQuery = { ...query, searchTarget: 'search-index' } | ||
260 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) | ||
261 | } | ||
262 | |||
263 | await updateSearchIndex(server, true, false) | ||
264 | |||
265 | { | ||
266 | const customQuery = { ...query, searchTarget: 'local' } | ||
267 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) | ||
268 | } | ||
269 | |||
270 | await updateSearchIndex(server, false, false) | ||
271 | } | ||
272 | }) | ||
273 | }) | ||
274 | |||
275 | after(async function () { | ||
276 | await cleanupTests([ server ]) | ||
277 | }) | ||
278 | }) | ||
diff --git a/packages/tests/src/api/check-params/services.ts b/packages/tests/src/api/check-params/services.ts new file mode 100644 index 000000000..0b0466d84 --- /dev/null +++ b/packages/tests/src/api/check-params/services.ts | |||
@@ -0,0 +1,207 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { | ||
4 | HttpStatusCode, | ||
5 | HttpStatusCodeType, | ||
6 | VideoCreateResult, | ||
7 | VideoPlaylistCreateResult, | ||
8 | VideoPlaylistPrivacy, | ||
9 | VideoPrivacy | ||
10 | } from '@peertube/peertube-models' | ||
11 | import { | ||
12 | cleanupTests, | ||
13 | createSingleServer, | ||
14 | makeGetRequest, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | |||
20 | describe('Test services API validators', function () { | ||
21 | let server: PeerTubeServer | ||
22 | let playlistUUID: string | ||
23 | |||
24 | let privateVideo: VideoCreateResult | ||
25 | let unlistedVideo: VideoCreateResult | ||
26 | |||
27 | let privatePlaylist: VideoPlaylistCreateResult | ||
28 | let unlistedPlaylist: VideoPlaylistCreateResult | ||
29 | |||
30 | // --------------------------------------------------------------- | ||
31 | |||
32 | before(async function () { | ||
33 | this.timeout(60000) | ||
34 | |||
35 | server = await createSingleServer(1) | ||
36 | await setAccessTokensToServers([ server ]) | ||
37 | await setDefaultVideoChannel([ server ]) | ||
38 | |||
39 | server.store.videoCreated = await server.videos.upload({ attributes: { name: 'my super name' } }) | ||
40 | |||
41 | privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE }) | ||
42 | unlistedVideo = await server.videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED }) | ||
43 | |||
44 | { | ||
45 | const created = await server.playlists.create({ | ||
46 | attributes: { | ||
47 | displayName: 'super playlist', | ||
48 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
49 | videoChannelId: server.store.channel.id | ||
50 | } | ||
51 | }) | ||
52 | |||
53 | playlistUUID = created.uuid | ||
54 | |||
55 | privatePlaylist = await server.playlists.create({ | ||
56 | attributes: { | ||
57 | displayName: 'private', | ||
58 | privacy: VideoPlaylistPrivacy.PRIVATE, | ||
59 | videoChannelId: server.store.channel.id | ||
60 | } | ||
61 | }) | ||
62 | |||
63 | unlistedPlaylist = await server.playlists.create({ | ||
64 | attributes: { | ||
65 | displayName: 'unlisted', | ||
66 | privacy: VideoPlaylistPrivacy.UNLISTED, | ||
67 | videoChannelId: server.store.channel.id | ||
68 | } | ||
69 | }) | ||
70 | } | ||
71 | }) | ||
72 | |||
73 | describe('Test oEmbed API validators', function () { | ||
74 | |||
75 | it('Should fail with an invalid url', async function () { | ||
76 | const embedUrl = 'hello.com' | ||
77 | await checkParamEmbed(server, embedUrl) | ||
78 | }) | ||
79 | |||
80 | it('Should fail with an invalid host', async function () { | ||
81 | const embedUrl = 'http://hello.com/videos/watch/' + server.store.videoCreated.uuid | ||
82 | await checkParamEmbed(server, embedUrl) | ||
83 | }) | ||
84 | |||
85 | it('Should fail with an invalid element id', async function () { | ||
86 | const embedUrl = `${server.url}/videos/watch/blabla` | ||
87 | await checkParamEmbed(server, embedUrl) | ||
88 | }) | ||
89 | |||
90 | it('Should fail with an unknown element', async function () { | ||
91 | const embedUrl = `${server.url}/videos/watch/88fc0165-d1f0-4a35-a51a-3b47f668689c` | ||
92 | await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_FOUND_404) | ||
93 | }) | ||
94 | |||
95 | it('Should fail with an invalid path', async function () { | ||
96 | const embedUrl = `${server.url}/videos/watchs/${server.store.videoCreated.uuid}` | ||
97 | |||
98 | await checkParamEmbed(server, embedUrl) | ||
99 | }) | ||
100 | |||
101 | it('Should fail with an invalid max height', async function () { | ||
102 | const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` | ||
103 | |||
104 | await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxheight: 'hello' }) | ||
105 | }) | ||
106 | |||
107 | it('Should fail with an invalid max width', async function () { | ||
108 | const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` | ||
109 | |||
110 | await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxwidth: 'hello' }) | ||
111 | }) | ||
112 | |||
113 | it('Should fail with an invalid format', async function () { | ||
114 | const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` | ||
115 | |||
116 | await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { format: 'blabla' }) | ||
117 | }) | ||
118 | |||
119 | it('Should fail with a non supported format', async function () { | ||
120 | const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` | ||
121 | |||
122 | await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_IMPLEMENTED_501, { format: 'xml' }) | ||
123 | }) | ||
124 | |||
125 | it('Should fail with a private video', async function () { | ||
126 | const embedUrl = `${server.url}/videos/watch/${privateVideo.uuid}` | ||
127 | |||
128 | await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403) | ||
129 | }) | ||
130 | |||
131 | it('Should fail with an unlisted video with the int id', async function () { | ||
132 | const embedUrl = `${server.url}/videos/watch/${unlistedVideo.id}` | ||
133 | |||
134 | await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403) | ||
135 | }) | ||
136 | |||
137 | it('Should succeed with an unlisted video using the uuid id', async function () { | ||
138 | for (const uuid of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) { | ||
139 | const embedUrl = `${server.url}/videos/watch/${uuid}` | ||
140 | |||
141 | await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200) | ||
142 | } | ||
143 | }) | ||
144 | |||
145 | it('Should fail with a private playlist', async function () { | ||
146 | const embedUrl = `${server.url}/videos/watch/playlist/${privatePlaylist.uuid}` | ||
147 | |||
148 | await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403) | ||
149 | }) | ||
150 | |||
151 | it('Should fail with an unlisted playlist using the int id', async function () { | ||
152 | const embedUrl = `${server.url}/videos/watch/playlist/${unlistedPlaylist.id}` | ||
153 | |||
154 | await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403) | ||
155 | }) | ||
156 | |||
157 | it('Should succeed with an unlisted playlist using the uuid id', async function () { | ||
158 | for (const uuid of [ unlistedPlaylist.uuid, unlistedPlaylist.shortUUID ]) { | ||
159 | const embedUrl = `${server.url}/videos/watch/playlist/${uuid}` | ||
160 | |||
161 | await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200) | ||
162 | } | ||
163 | }) | ||
164 | |||
165 | it('Should succeed with the correct params with a video', async function () { | ||
166 | const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` | ||
167 | const query = { | ||
168 | format: 'json', | ||
169 | maxheight: 400, | ||
170 | maxwidth: 400 | ||
171 | } | ||
172 | |||
173 | await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200, query) | ||
174 | }) | ||
175 | |||
176 | it('Should succeed with the correct params with a playlist', async function () { | ||
177 | const embedUrl = `${server.url}/videos/watch/playlist/${playlistUUID}` | ||
178 | const query = { | ||
179 | format: 'json', | ||
180 | maxheight: 400, | ||
181 | maxwidth: 400 | ||
182 | } | ||
183 | |||
184 | await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200, query) | ||
185 | }) | ||
186 | }) | ||
187 | |||
188 | after(async function () { | ||
189 | await cleanupTests([ server ]) | ||
190 | }) | ||
191 | }) | ||
192 | |||
193 | function checkParamEmbed ( | ||
194 | server: PeerTubeServer, | ||
195 | embedUrl: string, | ||
196 | expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400, | ||
197 | query = {} | ||
198 | ) { | ||
199 | const path = '/services/oembed' | ||
200 | |||
201 | return makeGetRequest({ | ||
202 | url: server.url, | ||
203 | path, | ||
204 | query: Object.assign(query, { url: embedUrl }), | ||
205 | expectedStatus | ||
206 | }) | ||
207 | } | ||
diff --git a/packages/tests/src/api/check-params/transcoding.ts b/packages/tests/src/api/check-params/transcoding.ts new file mode 100644 index 000000000..50935c59e --- /dev/null +++ b/packages/tests/src/api/check-params/transcoding.ts | |||
@@ -0,0 +1,112 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode, UserRole } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | waitJobs | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test transcoding API validators', function () { | ||
14 | let servers: PeerTubeServer[] | ||
15 | |||
16 | let userToken: string | ||
17 | let moderatorToken: string | ||
18 | |||
19 | let remoteId: string | ||
20 | let validId: string | ||
21 | |||
22 | // --------------------------------------------------------------- | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(120000) | ||
26 | |||
27 | servers = await createMultipleServers(2) | ||
28 | await setAccessTokensToServers(servers) | ||
29 | |||
30 | await doubleFollow(servers[0], servers[1]) | ||
31 | |||
32 | userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) | ||
33 | moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) | ||
34 | |||
35 | { | ||
36 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) | ||
37 | remoteId = uuid | ||
38 | } | ||
39 | |||
40 | { | ||
41 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) | ||
42 | validId = uuid | ||
43 | } | ||
44 | |||
45 | await waitJobs(servers) | ||
46 | |||
47 | await servers[0].config.enableTranscoding() | ||
48 | }) | ||
49 | |||
50 | it('Should not run transcoding of a unknown video', async function () { | ||
51 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
52 | await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'web-video', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
53 | }) | ||
54 | |||
55 | it('Should not run transcoding of a remote video', async function () { | ||
56 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
57 | |||
58 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus }) | ||
59 | await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'web-video', expectedStatus }) | ||
60 | }) | ||
61 | |||
62 | it('Should not run transcoding by a non admin user', async function () { | ||
63 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 | ||
64 | |||
65 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus }) | ||
66 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', token: moderatorToken, expectedStatus }) | ||
67 | }) | ||
68 | |||
69 | it('Should not run transcoding without transcoding type', async function () { | ||
70 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
71 | }) | ||
72 | |||
73 | it('Should not run transcoding with an incorrect transcoding type', async function () { | ||
74 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
75 | |||
76 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'toto' as any, expectedStatus }) | ||
77 | }) | ||
78 | |||
79 | it('Should not run transcoding if the instance disabled it', async function () { | ||
80 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
81 | |||
82 | await servers[0].config.disableTranscoding() | ||
83 | |||
84 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus }) | ||
85 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) | ||
86 | }) | ||
87 | |||
88 | it('Should run transcoding', async function () { | ||
89 | this.timeout(120_000) | ||
90 | |||
91 | await servers[0].config.enableTranscoding() | ||
92 | |||
93 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) | ||
94 | await waitJobs(servers) | ||
95 | |||
96 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true }) | ||
97 | await waitJobs(servers) | ||
98 | }) | ||
99 | |||
100 | it('Should not run transcoding on a video that is already being transcoded if forceTranscoding is not set', async function () { | ||
101 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) | ||
102 | |||
103 | const expectedStatus = HttpStatusCode.CONFLICT_409 | ||
104 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) | ||
105 | |||
106 | await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true }) | ||
107 | }) | ||
108 | |||
109 | after(async function () { | ||
110 | await cleanupTests(servers) | ||
111 | }) | ||
112 | }) | ||
diff --git a/packages/tests/src/api/check-params/two-factor.ts b/packages/tests/src/api/check-params/two-factor.ts new file mode 100644 index 000000000..0b1766eca --- /dev/null +++ b/packages/tests/src/api/check-params/two-factor.ts | |||
@@ -0,0 +1,294 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | PeerTubeServer, | ||
8 | setAccessTokensToServers, | ||
9 | TwoFactorCommand | ||
10 | } from '@peertube/peertube-server-commands' | ||
11 | |||
12 | describe('Test two factor API validators', function () { | ||
13 | let server: PeerTubeServer | ||
14 | |||
15 | let rootId: number | ||
16 | let rootPassword: string | ||
17 | let rootRequestToken: string | ||
18 | let rootOTPToken: string | ||
19 | |||
20 | let userId: number | ||
21 | let userToken = '' | ||
22 | let userPassword: string | ||
23 | let userRequestToken: string | ||
24 | let userOTPToken: string | ||
25 | |||
26 | // --------------------------------------------------------------- | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(30000) | ||
30 | |||
31 | { | ||
32 | server = await createSingleServer(1) | ||
33 | await setAccessTokensToServers([ server ]) | ||
34 | } | ||
35 | |||
36 | { | ||
37 | const result = await server.users.generate('user1') | ||
38 | userToken = result.token | ||
39 | userId = result.userId | ||
40 | userPassword = result.password | ||
41 | } | ||
42 | |||
43 | { | ||
44 | const { id } = await server.users.getMyInfo() | ||
45 | rootId = id | ||
46 | rootPassword = server.store.user.password | ||
47 | } | ||
48 | }) | ||
49 | |||
50 | describe('When requesting two factor', function () { | ||
51 | |||
52 | it('Should fail with an unknown user id', async function () { | ||
53 | await server.twoFactor.request({ userId: 42, currentPassword: rootPassword, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
54 | }) | ||
55 | |||
56 | it('Should fail with an invalid user id', async function () { | ||
57 | await server.twoFactor.request({ | ||
58 | userId: 'invalid' as any, | ||
59 | currentPassword: rootPassword, | ||
60 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
61 | }) | ||
62 | }) | ||
63 | |||
64 | it('Should fail to request another user two factor without the appropriate rights', async function () { | ||
65 | await server.twoFactor.request({ | ||
66 | userId: rootId, | ||
67 | token: userToken, | ||
68 | currentPassword: userPassword, | ||
69 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
70 | }) | ||
71 | }) | ||
72 | |||
73 | it('Should succeed to request another user two factor with the appropriate rights', async function () { | ||
74 | await server.twoFactor.request({ userId, currentPassword: rootPassword }) | ||
75 | }) | ||
76 | |||
77 | it('Should fail to request two factor without a password', async function () { | ||
78 | await server.twoFactor.request({ | ||
79 | userId, | ||
80 | token: userToken, | ||
81 | currentPassword: undefined, | ||
82 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
83 | }) | ||
84 | }) | ||
85 | |||
86 | it('Should fail to request two factor with an incorrect password', async function () { | ||
87 | await server.twoFactor.request({ | ||
88 | userId, | ||
89 | token: userToken, | ||
90 | currentPassword: rootPassword, | ||
91 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
92 | }) | ||
93 | }) | ||
94 | |||
95 | it('Should succeed to request two factor without a password when targeting a remote user with an admin account', async function () { | ||
96 | await server.twoFactor.request({ userId }) | ||
97 | }) | ||
98 | |||
99 | it('Should fail to request two factor without a password when targeting myself with an admin account', async function () { | ||
100 | await server.twoFactor.request({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
101 | await server.twoFactor.request({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
102 | }) | ||
103 | |||
104 | it('Should succeed to request my two factor auth', async function () { | ||
105 | { | ||
106 | const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword }) | ||
107 | userRequestToken = otpRequest.requestToken | ||
108 | userOTPToken = TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate() | ||
109 | } | ||
110 | |||
111 | { | ||
112 | const { otpRequest } = await server.twoFactor.request({ userId: rootId, currentPassword: rootPassword }) | ||
113 | rootRequestToken = otpRequest.requestToken | ||
114 | rootOTPToken = TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate() | ||
115 | } | ||
116 | }) | ||
117 | }) | ||
118 | |||
119 | describe('When confirming two factor request', function () { | ||
120 | |||
121 | it('Should fail with an unknown user id', async function () { | ||
122 | await server.twoFactor.confirmRequest({ | ||
123 | userId: 42, | ||
124 | requestToken: rootRequestToken, | ||
125 | otpToken: rootOTPToken, | ||
126 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
127 | }) | ||
128 | }) | ||
129 | |||
130 | it('Should fail with an invalid user id', async function () { | ||
131 | await server.twoFactor.confirmRequest({ | ||
132 | userId: 'invalid' as any, | ||
133 | requestToken: rootRequestToken, | ||
134 | otpToken: rootOTPToken, | ||
135 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
136 | }) | ||
137 | }) | ||
138 | |||
139 | it('Should fail to confirm another user two factor request without the appropriate rights', async function () { | ||
140 | await server.twoFactor.confirmRequest({ | ||
141 | userId: rootId, | ||
142 | token: userToken, | ||
143 | requestToken: rootRequestToken, | ||
144 | otpToken: rootOTPToken, | ||
145 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
146 | }) | ||
147 | }) | ||
148 | |||
149 | it('Should fail without request token', async function () { | ||
150 | await server.twoFactor.confirmRequest({ | ||
151 | userId, | ||
152 | requestToken: undefined, | ||
153 | otpToken: userOTPToken, | ||
154 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
155 | }) | ||
156 | }) | ||
157 | |||
158 | it('Should fail with an invalid request token', async function () { | ||
159 | await server.twoFactor.confirmRequest({ | ||
160 | userId, | ||
161 | requestToken: 'toto', | ||
162 | otpToken: userOTPToken, | ||
163 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
164 | }) | ||
165 | }) | ||
166 | |||
167 | it('Should fail with request token of another user', async function () { | ||
168 | await server.twoFactor.confirmRequest({ | ||
169 | userId, | ||
170 | requestToken: rootRequestToken, | ||
171 | otpToken: userOTPToken, | ||
172 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
173 | }) | ||
174 | }) | ||
175 | |||
176 | it('Should fail without an otp token', async function () { | ||
177 | await server.twoFactor.confirmRequest({ | ||
178 | userId, | ||
179 | requestToken: userRequestToken, | ||
180 | otpToken: undefined, | ||
181 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
182 | }) | ||
183 | }) | ||
184 | |||
185 | it('Should fail with a bad otp token', async function () { | ||
186 | await server.twoFactor.confirmRequest({ | ||
187 | userId, | ||
188 | requestToken: userRequestToken, | ||
189 | otpToken: '123456', | ||
190 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
191 | }) | ||
192 | }) | ||
193 | |||
194 | it('Should succeed to confirm another user two factor request with the appropriate rights', async function () { | ||
195 | await server.twoFactor.confirmRequest({ | ||
196 | userId, | ||
197 | requestToken: userRequestToken, | ||
198 | otpToken: userOTPToken | ||
199 | }) | ||
200 | |||
201 | // Reinit | ||
202 | await server.twoFactor.disable({ userId, currentPassword: rootPassword }) | ||
203 | }) | ||
204 | |||
205 | it('Should succeed to confirm my two factor request', async function () { | ||
206 | await server.twoFactor.confirmRequest({ | ||
207 | userId, | ||
208 | token: userToken, | ||
209 | requestToken: userRequestToken, | ||
210 | otpToken: userOTPToken | ||
211 | }) | ||
212 | }) | ||
213 | |||
214 | it('Should fail to confirm again two factor request', async function () { | ||
215 | await server.twoFactor.confirmRequest({ | ||
216 | userId, | ||
217 | token: userToken, | ||
218 | requestToken: userRequestToken, | ||
219 | otpToken: userOTPToken, | ||
220 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
221 | }) | ||
222 | }) | ||
223 | }) | ||
224 | |||
225 | describe('When disabling two factor', function () { | ||
226 | |||
227 | it('Should fail with an unknown user id', async function () { | ||
228 | await server.twoFactor.disable({ | ||
229 | userId: 42, | ||
230 | currentPassword: rootPassword, | ||
231 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
232 | }) | ||
233 | }) | ||
234 | |||
235 | it('Should fail with an invalid user id', async function () { | ||
236 | await server.twoFactor.disable({ | ||
237 | userId: 'invalid' as any, | ||
238 | currentPassword: rootPassword, | ||
239 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
240 | }) | ||
241 | }) | ||
242 | |||
243 | it('Should fail to disable another user two factor without the appropriate rights', async function () { | ||
244 | await server.twoFactor.disable({ | ||
245 | userId: rootId, | ||
246 | token: userToken, | ||
247 | currentPassword: userPassword, | ||
248 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
249 | }) | ||
250 | }) | ||
251 | |||
252 | it('Should fail to disable two factor with an incorrect password', async function () { | ||
253 | await server.twoFactor.disable({ | ||
254 | userId, | ||
255 | token: userToken, | ||
256 | currentPassword: rootPassword, | ||
257 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
258 | }) | ||
259 | }) | ||
260 | |||
261 | it('Should succeed to disable two factor without a password when targeting a remote user with an admin account', async function () { | ||
262 | await server.twoFactor.disable({ userId }) | ||
263 | await server.twoFactor.requestAndConfirm({ userId }) | ||
264 | }) | ||
265 | |||
266 | it('Should fail to disable two factor without a password when targeting myself with an admin account', async function () { | ||
267 | await server.twoFactor.disable({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
268 | await server.twoFactor.disable({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
269 | }) | ||
270 | |||
271 | it('Should succeed to disable another user two factor with the appropriate rights', async function () { | ||
272 | await server.twoFactor.disable({ userId, currentPassword: rootPassword }) | ||
273 | |||
274 | await server.twoFactor.requestAndConfirm({ userId }) | ||
275 | }) | ||
276 | |||
277 | it('Should succeed to update my two factor auth', async function () { | ||
278 | await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword }) | ||
279 | }) | ||
280 | |||
281 | it('Should fail to disable again two factor', async function () { | ||
282 | await server.twoFactor.disable({ | ||
283 | userId, | ||
284 | token: userToken, | ||
285 | currentPassword: userPassword, | ||
286 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
287 | }) | ||
288 | }) | ||
289 | }) | ||
290 | |||
291 | after(async function () { | ||
292 | await cleanupTests([ server ]) | ||
293 | }) | ||
294 | }) | ||
diff --git a/packages/tests/src/api/check-params/upload-quota.ts b/packages/tests/src/api/check-params/upload-quota.ts new file mode 100644 index 000000000..a77792822 --- /dev/null +++ b/packages/tests/src/api/check-params/upload-quota.ts | |||
@@ -0,0 +1,134 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
5 | import { randomInt } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, VideoImportState, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | VideosCommand, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test upload quota', function () { | ||
18 | let server: PeerTubeServer | ||
19 | let rootId: number | ||
20 | let command: VideosCommand | ||
21 | |||
22 | // --------------------------------------------------------------- | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await createSingleServer(1) | ||
28 | await setAccessTokensToServers([ server ]) | ||
29 | await setDefaultVideoChannel([ server ]) | ||
30 | |||
31 | const user = await server.users.getMyInfo() | ||
32 | rootId = user.id | ||
33 | |||
34 | await server.users.update({ userId: rootId, videoQuota: 42 }) | ||
35 | |||
36 | command = server.videos | ||
37 | }) | ||
38 | |||
39 | describe('When having a video quota', function () { | ||
40 | |||
41 | it('Should fail with a registered user having too many videos with legacy upload', async function () { | ||
42 | this.timeout(120000) | ||
43 | |||
44 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } | ||
45 | await server.registrations.register(user) | ||
46 | const userToken = await server.login.getAccessToken(user) | ||
47 | |||
48 | const attributes = { fixture: 'video_short2.webm' } | ||
49 | for (let i = 0; i < 5; i++) { | ||
50 | await command.upload({ token: userToken, attributes }) | ||
51 | } | ||
52 | |||
53 | await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' }) | ||
54 | }) | ||
55 | |||
56 | it('Should fail with a registered user having too many videos with resumable upload', async function () { | ||
57 | this.timeout(120000) | ||
58 | |||
59 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } | ||
60 | await server.registrations.register(user) | ||
61 | const userToken = await server.login.getAccessToken(user) | ||
62 | |||
63 | const attributes = { fixture: 'video_short2.webm' } | ||
64 | for (let i = 0; i < 5; i++) { | ||
65 | await command.upload({ token: userToken, attributes }) | ||
66 | } | ||
67 | |||
68 | await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' }) | ||
69 | }) | ||
70 | |||
71 | it('Should fail to import with HTTP/Torrent/magnet', async function () { | ||
72 | this.timeout(120_000) | ||
73 | |||
74 | const baseAttributes = { | ||
75 | channelId: server.store.channel.id, | ||
76 | privacy: VideoPrivacy.PUBLIC | ||
77 | } | ||
78 | await server.imports.importVideo({ attributes: { ...baseAttributes, targetUrl: FIXTURE_URLS.goodVideo } }) | ||
79 | await server.imports.importVideo({ attributes: { ...baseAttributes, magnetUri: FIXTURE_URLS.magnet } }) | ||
80 | await server.imports.importVideo({ attributes: { ...baseAttributes, torrentfile: 'video-720p.torrent' as any } }) | ||
81 | |||
82 | await waitJobs([ server ]) | ||
83 | |||
84 | const { total, data: videoImports } = await server.imports.getMyVideoImports() | ||
85 | expect(total).to.equal(3) | ||
86 | |||
87 | expect(videoImports).to.have.lengthOf(3) | ||
88 | |||
89 | for (const videoImport of videoImports) { | ||
90 | expect(videoImport.state.id).to.equal(VideoImportState.FAILED) | ||
91 | expect(videoImport.error).not.to.be.undefined | ||
92 | expect(videoImport.error).to.contain('user video quota is exceeded') | ||
93 | } | ||
94 | }) | ||
95 | }) | ||
96 | |||
97 | describe('When having a daily video quota', function () { | ||
98 | |||
99 | it('Should fail with a user having too many videos daily', async function () { | ||
100 | await server.users.update({ userId: rootId, videoQuotaDaily: 42 }) | ||
101 | |||
102 | await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' }) | ||
103 | await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' }) | ||
104 | }) | ||
105 | }) | ||
106 | |||
107 | describe('When having an absolute and daily video quota', function () { | ||
108 | it('Should fail if exceeding total quota', async function () { | ||
109 | await server.users.update({ | ||
110 | userId: rootId, | ||
111 | videoQuota: 42, | ||
112 | videoQuotaDaily: 1024 * 1024 * 1024 | ||
113 | }) | ||
114 | |||
115 | await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' }) | ||
116 | await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' }) | ||
117 | }) | ||
118 | |||
119 | it('Should fail if exceeding daily quota', async function () { | ||
120 | await server.users.update({ | ||
121 | userId: rootId, | ||
122 | videoQuota: 1024 * 1024 * 1024, | ||
123 | videoQuotaDaily: 42 | ||
124 | }) | ||
125 | |||
126 | await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' }) | ||
127 | await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' }) | ||
128 | }) | ||
129 | }) | ||
130 | |||
131 | after(async function () { | ||
132 | await cleanupTests([ server ]) | ||
133 | }) | ||
134 | }) | ||
diff --git a/packages/tests/src/api/check-params/user-notifications.ts b/packages/tests/src/api/check-params/user-notifications.ts new file mode 100644 index 000000000..cf20324a1 --- /dev/null +++ b/packages/tests/src/api/check-params/user-notifications.ts | |||
@@ -0,0 +1,290 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { io } from 'socket.io-client' | ||
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, UserNotificationSetting, UserNotificationSettingValue } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | makePutBodyRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test user notifications API validators', function () { | ||
18 | let server: PeerTubeServer | ||
19 | |||
20 | // --------------------------------------------------------------- | ||
21 | |||
22 | before(async function () { | ||
23 | this.timeout(30000) | ||
24 | |||
25 | server = await createSingleServer(1) | ||
26 | |||
27 | await setAccessTokensToServers([ server ]) | ||
28 | }) | ||
29 | |||
30 | describe('When listing my notifications', function () { | ||
31 | const path = '/api/v1/users/me/notifications' | ||
32 | |||
33 | it('Should fail with a bad start pagination', async function () { | ||
34 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
35 | }) | ||
36 | |||
37 | it('Should fail with a bad count pagination', async function () { | ||
38 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
39 | }) | ||
40 | |||
41 | it('Should fail with an incorrect sort', async function () { | ||
42 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
43 | }) | ||
44 | |||
45 | it('Should fail with an incorrect unread parameter', async function () { | ||
46 | await makeGetRequest({ | ||
47 | url: server.url, | ||
48 | path, | ||
49 | query: { | ||
50 | unread: 'toto' | ||
51 | }, | ||
52 | token: server.accessToken, | ||
53 | expectedStatus: HttpStatusCode.OK_200 | ||
54 | }) | ||
55 | }) | ||
56 | |||
57 | it('Should fail with a non authenticated user', async function () { | ||
58 | await makeGetRequest({ | ||
59 | url: server.url, | ||
60 | path, | ||
61 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
62 | }) | ||
63 | }) | ||
64 | |||
65 | it('Should succeed with the correct parameters', async function () { | ||
66 | await makeGetRequest({ | ||
67 | url: server.url, | ||
68 | path, | ||
69 | token: server.accessToken, | ||
70 | expectedStatus: HttpStatusCode.OK_200 | ||
71 | }) | ||
72 | }) | ||
73 | }) | ||
74 | |||
75 | describe('When marking as read my notifications', function () { | ||
76 | const path = '/api/v1/users/me/notifications/read' | ||
77 | |||
78 | it('Should fail with wrong ids parameters', async function () { | ||
79 | await makePostBodyRequest({ | ||
80 | url: server.url, | ||
81 | path, | ||
82 | fields: { | ||
83 | ids: [ 'hello' ] | ||
84 | }, | ||
85 | token: server.accessToken, | ||
86 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
87 | }) | ||
88 | |||
89 | await makePostBodyRequest({ | ||
90 | url: server.url, | ||
91 | path, | ||
92 | fields: { | ||
93 | ids: [ ] | ||
94 | }, | ||
95 | token: server.accessToken, | ||
96 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
97 | }) | ||
98 | |||
99 | await makePostBodyRequest({ | ||
100 | url: server.url, | ||
101 | path, | ||
102 | fields: { | ||
103 | ids: 5 | ||
104 | }, | ||
105 | token: server.accessToken, | ||
106 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
107 | }) | ||
108 | }) | ||
109 | |||
110 | it('Should fail with a non authenticated user', async function () { | ||
111 | await makePostBodyRequest({ | ||
112 | url: server.url, | ||
113 | path, | ||
114 | fields: { | ||
115 | ids: [ 5 ] | ||
116 | }, | ||
117 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
118 | }) | ||
119 | }) | ||
120 | |||
121 | it('Should succeed with the correct parameters', async function () { | ||
122 | await makePostBodyRequest({ | ||
123 | url: server.url, | ||
124 | path, | ||
125 | fields: { | ||
126 | ids: [ 5 ] | ||
127 | }, | ||
128 | token: server.accessToken, | ||
129 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
130 | }) | ||
131 | }) | ||
132 | }) | ||
133 | |||
134 | describe('When marking as read my notifications', function () { | ||
135 | const path = '/api/v1/users/me/notifications/read-all' | ||
136 | |||
137 | it('Should fail with a non authenticated user', async function () { | ||
138 | await makePostBodyRequest({ | ||
139 | url: server.url, | ||
140 | path, | ||
141 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
142 | }) | ||
143 | }) | ||
144 | |||
145 | it('Should succeed with the correct parameters', async function () { | ||
146 | await makePostBodyRequest({ | ||
147 | url: server.url, | ||
148 | path, | ||
149 | token: server.accessToken, | ||
150 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
151 | }) | ||
152 | }) | ||
153 | }) | ||
154 | |||
155 | describe('When updating my notification settings', function () { | ||
156 | const path = '/api/v1/users/me/notification-settings' | ||
157 | const correctFields: UserNotificationSetting = { | ||
158 | newVideoFromSubscription: UserNotificationSettingValue.WEB, | ||
159 | newCommentOnMyVideo: UserNotificationSettingValue.WEB, | ||
160 | abuseAsModerator: UserNotificationSettingValue.WEB, | ||
161 | videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB, | ||
162 | blacklistOnMyVideo: UserNotificationSettingValue.WEB, | ||
163 | myVideoImportFinished: UserNotificationSettingValue.WEB, | ||
164 | myVideoPublished: UserNotificationSettingValue.WEB, | ||
165 | commentMention: UserNotificationSettingValue.WEB, | ||
166 | newFollow: UserNotificationSettingValue.WEB, | ||
167 | newUserRegistration: UserNotificationSettingValue.WEB, | ||
168 | newInstanceFollower: UserNotificationSettingValue.WEB, | ||
169 | autoInstanceFollowing: UserNotificationSettingValue.WEB, | ||
170 | abuseNewMessage: UserNotificationSettingValue.WEB, | ||
171 | abuseStateChange: UserNotificationSettingValue.WEB, | ||
172 | newPeerTubeVersion: UserNotificationSettingValue.WEB, | ||
173 | myVideoStudioEditionFinished: UserNotificationSettingValue.WEB, | ||
174 | newPluginVersion: UserNotificationSettingValue.WEB | ||
175 | } | ||
176 | |||
177 | it('Should fail with missing fields', async function () { | ||
178 | await makePutBodyRequest({ | ||
179 | url: server.url, | ||
180 | path, | ||
181 | token: server.accessToken, | ||
182 | fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB }, | ||
183 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
184 | }) | ||
185 | }) | ||
186 | |||
187 | it('Should fail with incorrect field values', async function () { | ||
188 | { | ||
189 | const fields = { ...correctFields, newCommentOnMyVideo: 15 } | ||
190 | |||
191 | await makePutBodyRequest({ | ||
192 | url: server.url, | ||
193 | path, | ||
194 | token: server.accessToken, | ||
195 | fields, | ||
196 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
197 | }) | ||
198 | } | ||
199 | |||
200 | { | ||
201 | const fields = { ...correctFields, newCommentOnMyVideo: 'toto' } | ||
202 | |||
203 | await makePutBodyRequest({ | ||
204 | url: server.url, | ||
205 | path, | ||
206 | fields, | ||
207 | token: server.accessToken, | ||
208 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
209 | }) | ||
210 | } | ||
211 | }) | ||
212 | |||
213 | it('Should fail with a non authenticated user', async function () { | ||
214 | await makePutBodyRequest({ | ||
215 | url: server.url, | ||
216 | path, | ||
217 | fields: correctFields, | ||
218 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
219 | }) | ||
220 | }) | ||
221 | |||
222 | it('Should succeed with the correct parameters', async function () { | ||
223 | await makePutBodyRequest({ | ||
224 | url: server.url, | ||
225 | path, | ||
226 | token: server.accessToken, | ||
227 | fields: correctFields, | ||
228 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
229 | }) | ||
230 | }) | ||
231 | }) | ||
232 | |||
233 | describe('When connecting to my notification socket', function () { | ||
234 | |||
235 | it('Should fail with no token', function (next) { | ||
236 | const socket = io(`${server.url}/user-notifications`, { reconnection: false }) | ||
237 | |||
238 | socket.once('connect_error', function () { | ||
239 | socket.disconnect() | ||
240 | next() | ||
241 | }) | ||
242 | |||
243 | socket.on('connect', () => { | ||
244 | socket.disconnect() | ||
245 | next(new Error('Connected with a missing token.')) | ||
246 | }) | ||
247 | }) | ||
248 | |||
249 | it('Should fail with an invalid token', function (next) { | ||
250 | const socket = io(`${server.url}/user-notifications`, { | ||
251 | query: { accessToken: 'bad_access_token' }, | ||
252 | reconnection: false | ||
253 | }) | ||
254 | |||
255 | socket.once('connect_error', function () { | ||
256 | socket.disconnect() | ||
257 | next() | ||
258 | }) | ||
259 | |||
260 | socket.on('connect', () => { | ||
261 | socket.disconnect() | ||
262 | next(new Error('Connected with an invalid token.')) | ||
263 | }) | ||
264 | }) | ||
265 | |||
266 | it('Should success with the correct token', function (next) { | ||
267 | const socket = io(`${server.url}/user-notifications`, { | ||
268 | query: { accessToken: server.accessToken }, | ||
269 | reconnection: false | ||
270 | }) | ||
271 | |||
272 | function errorListener (err) { | ||
273 | next(new Error('Error in connection: ' + err)) | ||
274 | } | ||
275 | |||
276 | socket.on('connect_error', errorListener) | ||
277 | |||
278 | socket.once('connect', async () => { | ||
279 | socket.disconnect() | ||
280 | |||
281 | await wait(500) | ||
282 | next() | ||
283 | }) | ||
284 | }) | ||
285 | }) | ||
286 | |||
287 | after(async function () { | ||
288 | await cleanupTests([ server ]) | ||
289 | }) | ||
290 | }) | ||
diff --git a/packages/tests/src/api/check-params/user-subscriptions.ts b/packages/tests/src/api/check-params/user-subscriptions.ts new file mode 100644 index 000000000..e97f513a0 --- /dev/null +++ b/packages/tests/src/api/check-params/user-subscriptions.ts | |||
@@ -0,0 +1,298 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { | ||
4 | cleanupTests, | ||
5 | createSingleServer, | ||
6 | makeDeleteRequest, | ||
7 | makeGetRequest, | ||
8 | makePostBodyRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
14 | import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js' | ||
15 | |||
16 | describe('Test user subscriptions API validators', function () { | ||
17 | const path = '/api/v1/users/me/subscriptions' | ||
18 | let server: PeerTubeServer | ||
19 | let userAccessToken = '' | ||
20 | |||
21 | // --------------------------------------------------------------- | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(30000) | ||
25 | |||
26 | server = await createSingleServer(1) | ||
27 | |||
28 | await setAccessTokensToServers([ server ]) | ||
29 | |||
30 | const user = { | ||
31 | username: 'user1', | ||
32 | password: 'my super password' | ||
33 | } | ||
34 | await server.users.create({ username: user.username, password: user.password }) | ||
35 | userAccessToken = await server.login.getAccessToken(user) | ||
36 | }) | ||
37 | |||
38 | describe('When listing my subscriptions', function () { | ||
39 | it('Should fail with a bad start pagination', async function () { | ||
40 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
41 | }) | ||
42 | |||
43 | it('Should fail with a bad count pagination', async function () { | ||
44 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
45 | }) | ||
46 | |||
47 | it('Should fail with an incorrect sort', async function () { | ||
48 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
49 | }) | ||
50 | |||
51 | it('Should fail with a non authenticated user', async function () { | ||
52 | await makeGetRequest({ | ||
53 | url: server.url, | ||
54 | path, | ||
55 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
56 | }) | ||
57 | }) | ||
58 | |||
59 | it('Should succeed with the correct parameters', async function () { | ||
60 | await makeGetRequest({ | ||
61 | url: server.url, | ||
62 | path, | ||
63 | token: userAccessToken, | ||
64 | expectedStatus: HttpStatusCode.OK_200 | ||
65 | }) | ||
66 | }) | ||
67 | }) | ||
68 | |||
69 | describe('When listing my subscriptions videos', function () { | ||
70 | const path = '/api/v1/users/me/subscriptions/videos' | ||
71 | |||
72 | it('Should fail with a bad start pagination', async function () { | ||
73 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
74 | }) | ||
75 | |||
76 | it('Should fail with a bad count pagination', async function () { | ||
77 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
78 | }) | ||
79 | |||
80 | it('Should fail with an incorrect sort', async function () { | ||
81 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
82 | }) | ||
83 | |||
84 | it('Should fail with a non authenticated user', async function () { | ||
85 | await makeGetRequest({ | ||
86 | url: server.url, | ||
87 | path, | ||
88 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
89 | }) | ||
90 | }) | ||
91 | |||
92 | it('Should succeed with the correct parameters', async function () { | ||
93 | await makeGetRequest({ | ||
94 | url: server.url, | ||
95 | path, | ||
96 | token: userAccessToken, | ||
97 | expectedStatus: HttpStatusCode.OK_200 | ||
98 | }) | ||
99 | }) | ||
100 | }) | ||
101 | |||
102 | describe('When adding a subscription', function () { | ||
103 | it('Should fail with a non authenticated user', async function () { | ||
104 | await makePostBodyRequest({ | ||
105 | url: server.url, | ||
106 | path, | ||
107 | fields: { uri: 'user1_channel@' + server.host }, | ||
108 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
109 | }) | ||
110 | }) | ||
111 | |||
112 | it('Should fail with bad URIs', async function () { | ||
113 | await makePostBodyRequest({ | ||
114 | url: server.url, | ||
115 | path, | ||
116 | token: server.accessToken, | ||
117 | fields: { uri: 'root' }, | ||
118 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
119 | }) | ||
120 | |||
121 | await makePostBodyRequest({ | ||
122 | url: server.url, | ||
123 | path, | ||
124 | token: server.accessToken, | ||
125 | fields: { uri: 'root@' }, | ||
126 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
127 | }) | ||
128 | |||
129 | await makePostBodyRequest({ | ||
130 | url: server.url, | ||
131 | path, | ||
132 | token: server.accessToken, | ||
133 | fields: { uri: 'root@hello@' }, | ||
134 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
135 | }) | ||
136 | }) | ||
137 | |||
138 | it('Should succeed with the correct parameters', async function () { | ||
139 | this.timeout(20000) | ||
140 | |||
141 | await makePostBodyRequest({ | ||
142 | url: server.url, | ||
143 | path, | ||
144 | token: server.accessToken, | ||
145 | fields: { uri: 'user1_channel@' + server.host }, | ||
146 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
147 | }) | ||
148 | |||
149 | await waitJobs([ server ]) | ||
150 | }) | ||
151 | }) | ||
152 | |||
153 | describe('When getting a subscription', function () { | ||
154 | it('Should fail with a non authenticated user', async function () { | ||
155 | await makeGetRequest({ | ||
156 | url: server.url, | ||
157 | path: path + '/user1_channel@' + server.host, | ||
158 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
159 | }) | ||
160 | }) | ||
161 | |||
162 | it('Should fail with bad URIs', async function () { | ||
163 | await makeGetRequest({ | ||
164 | url: server.url, | ||
165 | path: path + '/root', | ||
166 | token: server.accessToken, | ||
167 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
168 | }) | ||
169 | |||
170 | await makeGetRequest({ | ||
171 | url: server.url, | ||
172 | path: path + '/root@', | ||
173 | token: server.accessToken, | ||
174 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
175 | }) | ||
176 | |||
177 | await makeGetRequest({ | ||
178 | url: server.url, | ||
179 | path: path + '/root@hello@', | ||
180 | token: server.accessToken, | ||
181 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
182 | }) | ||
183 | }) | ||
184 | |||
185 | it('Should fail with an unknown subscription', async function () { | ||
186 | await makeGetRequest({ | ||
187 | url: server.url, | ||
188 | path: path + '/root1@' + server.host, | ||
189 | token: server.accessToken, | ||
190 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
191 | }) | ||
192 | }) | ||
193 | |||
194 | it('Should succeed with the correct parameters', async function () { | ||
195 | await makeGetRequest({ | ||
196 | url: server.url, | ||
197 | path: path + '/user1_channel@' + server.host, | ||
198 | token: server.accessToken, | ||
199 | expectedStatus: HttpStatusCode.OK_200 | ||
200 | }) | ||
201 | }) | ||
202 | }) | ||
203 | |||
204 | describe('When checking if subscriptions exist', function () { | ||
205 | const existPath = path + '/exist' | ||
206 | |||
207 | it('Should fail with a non authenticated user', async function () { | ||
208 | await makeGetRequest({ | ||
209 | url: server.url, | ||
210 | path: existPath, | ||
211 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
212 | }) | ||
213 | }) | ||
214 | |||
215 | it('Should fail with bad URIs', async function () { | ||
216 | await makeGetRequest({ | ||
217 | url: server.url, | ||
218 | path: existPath, | ||
219 | query: { uris: 'toto' }, | ||
220 | token: server.accessToken, | ||
221 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
222 | }) | ||
223 | |||
224 | await makeGetRequest({ | ||
225 | url: server.url, | ||
226 | path: existPath, | ||
227 | query: { 'uris[]': 1 }, | ||
228 | token: server.accessToken, | ||
229 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
230 | }) | ||
231 | }) | ||
232 | |||
233 | it('Should succeed with the correct parameters', async function () { | ||
234 | await makeGetRequest({ | ||
235 | url: server.url, | ||
236 | path: existPath, | ||
237 | query: { 'uris[]': 'coucou@' + server.host }, | ||
238 | token: server.accessToken, | ||
239 | expectedStatus: HttpStatusCode.OK_200 | ||
240 | }) | ||
241 | }) | ||
242 | }) | ||
243 | |||
244 | describe('When removing a subscription', function () { | ||
245 | it('Should fail with a non authenticated user', async function () { | ||
246 | await makeDeleteRequest({ | ||
247 | url: server.url, | ||
248 | path: path + '/user1_channel@' + server.host, | ||
249 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
250 | }) | ||
251 | }) | ||
252 | |||
253 | it('Should fail with bad URIs', async function () { | ||
254 | await makeDeleteRequest({ | ||
255 | url: server.url, | ||
256 | path: path + '/root', | ||
257 | token: server.accessToken, | ||
258 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
259 | }) | ||
260 | |||
261 | await makeDeleteRequest({ | ||
262 | url: server.url, | ||
263 | path: path + '/root@', | ||
264 | token: server.accessToken, | ||
265 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
266 | }) | ||
267 | |||
268 | await makeDeleteRequest({ | ||
269 | url: server.url, | ||
270 | path: path + '/root@hello@', | ||
271 | token: server.accessToken, | ||
272 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
273 | }) | ||
274 | }) | ||
275 | |||
276 | it('Should fail with an unknown subscription', async function () { | ||
277 | await makeDeleteRequest({ | ||
278 | url: server.url, | ||
279 | path: path + '/root1@' + server.host, | ||
280 | token: server.accessToken, | ||
281 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
282 | }) | ||
283 | }) | ||
284 | |||
285 | it('Should succeed with the correct parameters', async function () { | ||
286 | await makeDeleteRequest({ | ||
287 | url: server.url, | ||
288 | path: path + '/user1_channel@' + server.host, | ||
289 | token: server.accessToken, | ||
290 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
291 | }) | ||
292 | }) | ||
293 | }) | ||
294 | |||
295 | after(async function () { | ||
296 | await cleanupTests([ server ]) | ||
297 | }) | ||
298 | }) | ||
diff --git a/packages/tests/src/api/check-params/users-admin.ts b/packages/tests/src/api/check-params/users-admin.ts new file mode 100644 index 000000000..1ad222ddc --- /dev/null +++ b/packages/tests/src/api/check-params/users-admin.ts | |||
@@ -0,0 +1,457 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { omit } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, UserAdminFlag, UserRole } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | ConfigCommand, | ||
10 | createSingleServer, | ||
11 | killallServers, | ||
12 | makeGetRequest, | ||
13 | makePostBodyRequest, | ||
14 | makePutBodyRequest, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | |||
19 | describe('Test users admin API validators', function () { | ||
20 | const path = '/api/v1/users/' | ||
21 | let userId: number | ||
22 | let rootId: number | ||
23 | let moderatorId: number | ||
24 | let server: PeerTubeServer | ||
25 | let userToken = '' | ||
26 | let moderatorToken = '' | ||
27 | let emailPort: number | ||
28 | |||
29 | // --------------------------------------------------------------- | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(30000) | ||
33 | |||
34 | const emails: object[] = [] | ||
35 | emailPort = await MockSmtpServer.Instance.collectEmails(emails) | ||
36 | |||
37 | { | ||
38 | server = await createSingleServer(1) | ||
39 | |||
40 | await setAccessTokensToServers([ server ]) | ||
41 | } | ||
42 | |||
43 | { | ||
44 | const result = await server.users.generate('user1') | ||
45 | userToken = result.token | ||
46 | userId = result.userId | ||
47 | } | ||
48 | |||
49 | { | ||
50 | const result = await server.users.generate('moderator1', UserRole.MODERATOR) | ||
51 | moderatorToken = result.token | ||
52 | } | ||
53 | |||
54 | { | ||
55 | const result = await server.users.generate('moderator2', UserRole.MODERATOR) | ||
56 | moderatorId = result.userId | ||
57 | } | ||
58 | }) | ||
59 | |||
60 | describe('When listing users', function () { | ||
61 | it('Should fail with a bad start pagination', async function () { | ||
62 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
63 | }) | ||
64 | |||
65 | it('Should fail with a bad count pagination', async function () { | ||
66 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
67 | }) | ||
68 | |||
69 | it('Should fail with an incorrect sort', async function () { | ||
70 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
71 | }) | ||
72 | |||
73 | it('Should fail with a non authenticated user', async function () { | ||
74 | await makeGetRequest({ | ||
75 | url: server.url, | ||
76 | path, | ||
77 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
78 | }) | ||
79 | }) | ||
80 | |||
81 | it('Should fail with a non admin user', async function () { | ||
82 | await makeGetRequest({ | ||
83 | url: server.url, | ||
84 | path, | ||
85 | token: userToken, | ||
86 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
87 | }) | ||
88 | }) | ||
89 | }) | ||
90 | |||
91 | describe('When adding a new user', function () { | ||
92 | const baseCorrectParams = { | ||
93 | username: 'user2', | ||
94 | email: 'test@example.com', | ||
95 | password: 'my super password', | ||
96 | videoQuota: -1, | ||
97 | videoQuotaDaily: -1, | ||
98 | role: UserRole.USER, | ||
99 | adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST | ||
100 | } | ||
101 | |||
102 | it('Should fail with a too small username', async function () { | ||
103 | const fields = { ...baseCorrectParams, username: '' } | ||
104 | |||
105 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
106 | }) | ||
107 | |||
108 | it('Should fail with a too long username', async function () { | ||
109 | const fields = { ...baseCorrectParams, username: 'super'.repeat(50) } | ||
110 | |||
111 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
112 | }) | ||
113 | |||
114 | it('Should fail with a not lowercase username', async function () { | ||
115 | const fields = { ...baseCorrectParams, username: 'Toto' } | ||
116 | |||
117 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
118 | }) | ||
119 | |||
120 | it('Should fail with an incorrect username', async function () { | ||
121 | const fields = { ...baseCorrectParams, username: 'my username' } | ||
122 | |||
123 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
124 | }) | ||
125 | |||
126 | it('Should fail with a missing email', async function () { | ||
127 | const fields = omit(baseCorrectParams, [ 'email' ]) | ||
128 | |||
129 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
130 | }) | ||
131 | |||
132 | it('Should fail with an invalid email', async function () { | ||
133 | const fields = { ...baseCorrectParams, email: 'test_example.com' } | ||
134 | |||
135 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
136 | }) | ||
137 | |||
138 | it('Should fail with a too small password', async function () { | ||
139 | const fields = { ...baseCorrectParams, password: 'bla' } | ||
140 | |||
141 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
142 | }) | ||
143 | |||
144 | it('Should fail with a too long password', async function () { | ||
145 | const fields = { ...baseCorrectParams, password: 'super'.repeat(61) } | ||
146 | |||
147 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
148 | }) | ||
149 | |||
150 | it('Should fail with empty password and no smtp configured', async function () { | ||
151 | const fields = { ...baseCorrectParams, password: '' } | ||
152 | |||
153 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
154 | }) | ||
155 | |||
156 | it('Should succeed with no password on a server with smtp enabled', async function () { | ||
157 | this.timeout(20000) | ||
158 | |||
159 | await killallServers([ server ]) | ||
160 | |||
161 | await server.run(ConfigCommand.getEmailOverrideConfig(emailPort)) | ||
162 | |||
163 | const fields = { | ||
164 | ...baseCorrectParams, | ||
165 | |||
166 | password: '', | ||
167 | username: 'create_password', | ||
168 | email: 'create_password@example.com' | ||
169 | } | ||
170 | |||
171 | await makePostBodyRequest({ | ||
172 | url: server.url, | ||
173 | path, | ||
174 | token: server.accessToken, | ||
175 | fields, | ||
176 | expectedStatus: HttpStatusCode.OK_200 | ||
177 | }) | ||
178 | }) | ||
179 | |||
180 | it('Should fail with invalid admin flags', async function () { | ||
181 | const fields = { ...baseCorrectParams, adminFlags: 'toto' } | ||
182 | |||
183 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
184 | }) | ||
185 | |||
186 | it('Should fail with an non authenticated user', async function () { | ||
187 | await makePostBodyRequest({ | ||
188 | url: server.url, | ||
189 | path, | ||
190 | token: 'super token', | ||
191 | fields: baseCorrectParams, | ||
192 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
193 | }) | ||
194 | }) | ||
195 | |||
196 | it('Should fail if we add a user with the same username', async function () { | ||
197 | const fields = { ...baseCorrectParams, username: 'user1' } | ||
198 | |||
199 | await makePostBodyRequest({ | ||
200 | url: server.url, | ||
201 | path, | ||
202 | token: server.accessToken, | ||
203 | fields, | ||
204 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
205 | }) | ||
206 | }) | ||
207 | |||
208 | it('Should fail if we add a user with the same email', async function () { | ||
209 | const fields = { ...baseCorrectParams, email: 'user1@example.com' } | ||
210 | |||
211 | await makePostBodyRequest({ | ||
212 | url: server.url, | ||
213 | path, | ||
214 | token: server.accessToken, | ||
215 | fields, | ||
216 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
217 | }) | ||
218 | }) | ||
219 | |||
220 | it('Should fail with an invalid videoQuota', async function () { | ||
221 | const fields = { ...baseCorrectParams, videoQuota: -5 } | ||
222 | |||
223 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
224 | }) | ||
225 | |||
226 | it('Should fail with an invalid videoQuotaDaily', async function () { | ||
227 | const fields = { ...baseCorrectParams, videoQuotaDaily: -7 } | ||
228 | |||
229 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
230 | }) | ||
231 | |||
232 | it('Should fail without a user role', async function () { | ||
233 | const fields = omit(baseCorrectParams, [ 'role' ]) | ||
234 | |||
235 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
236 | }) | ||
237 | |||
238 | it('Should fail with an invalid user role', async function () { | ||
239 | const fields = { ...baseCorrectParams, role: 88989 } | ||
240 | |||
241 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
242 | }) | ||
243 | |||
244 | it('Should fail with a "peertube" username', async function () { | ||
245 | const fields = { ...baseCorrectParams, username: 'peertube' } | ||
246 | |||
247 | await makePostBodyRequest({ | ||
248 | url: server.url, | ||
249 | path, | ||
250 | token: server.accessToken, | ||
251 | fields, | ||
252 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
253 | }) | ||
254 | }) | ||
255 | |||
256 | it('Should fail to create a moderator or an admin with a moderator', async function () { | ||
257 | for (const role of [ UserRole.MODERATOR, UserRole.ADMINISTRATOR ]) { | ||
258 | const fields = { ...baseCorrectParams, role } | ||
259 | |||
260 | await makePostBodyRequest({ | ||
261 | url: server.url, | ||
262 | path, | ||
263 | token: moderatorToken, | ||
264 | fields, | ||
265 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
266 | }) | ||
267 | } | ||
268 | }) | ||
269 | |||
270 | it('Should succeed to create a user with a moderator', async function () { | ||
271 | const fields = { ...baseCorrectParams, username: 'a4656', email: 'a4656@example.com', role: UserRole.USER } | ||
272 | |||
273 | await makePostBodyRequest({ | ||
274 | url: server.url, | ||
275 | path, | ||
276 | token: moderatorToken, | ||
277 | fields, | ||
278 | expectedStatus: HttpStatusCode.OK_200 | ||
279 | }) | ||
280 | }) | ||
281 | |||
282 | it('Should succeed with the correct params', async function () { | ||
283 | await makePostBodyRequest({ | ||
284 | url: server.url, | ||
285 | path, | ||
286 | token: server.accessToken, | ||
287 | fields: baseCorrectParams, | ||
288 | expectedStatus: HttpStatusCode.OK_200 | ||
289 | }) | ||
290 | }) | ||
291 | |||
292 | it('Should fail with a non admin user', async function () { | ||
293 | const user = { username: 'user1' } | ||
294 | userToken = await server.login.getAccessToken(user) | ||
295 | |||
296 | const fields = { | ||
297 | username: 'user3', | ||
298 | email: 'test@example.com', | ||
299 | password: 'my super password', | ||
300 | videoQuota: 42000000 | ||
301 | } | ||
302 | await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
303 | }) | ||
304 | }) | ||
305 | |||
306 | describe('When getting a user', function () { | ||
307 | |||
308 | it('Should fail with an non authenticated user', async function () { | ||
309 | await makeGetRequest({ | ||
310 | url: server.url, | ||
311 | path: path + userId, | ||
312 | token: 'super token', | ||
313 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
314 | }) | ||
315 | }) | ||
316 | |||
317 | it('Should fail with a non admin user', async function () { | ||
318 | await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
319 | }) | ||
320 | |||
321 | it('Should succeed with the correct params', async function () { | ||
322 | await makeGetRequest({ url: server.url, path: path + userId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
323 | }) | ||
324 | }) | ||
325 | |||
326 | describe('When updating a user', function () { | ||
327 | |||
328 | it('Should fail with an invalid email attribute', async function () { | ||
329 | const fields = { | ||
330 | email: 'blabla' | ||
331 | } | ||
332 | |||
333 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
334 | }) | ||
335 | |||
336 | it('Should fail with an invalid emailVerified attribute', async function () { | ||
337 | const fields = { | ||
338 | emailVerified: 'yes' | ||
339 | } | ||
340 | |||
341 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
342 | }) | ||
343 | |||
344 | it('Should fail with an invalid videoQuota attribute', async function () { | ||
345 | const fields = { | ||
346 | videoQuota: -90 | ||
347 | } | ||
348 | |||
349 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
350 | }) | ||
351 | |||
352 | it('Should fail with an invalid user role attribute', async function () { | ||
353 | const fields = { | ||
354 | role: 54878 | ||
355 | } | ||
356 | |||
357 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
358 | }) | ||
359 | |||
360 | it('Should fail with a too small password', async function () { | ||
361 | const fields = { | ||
362 | currentPassword: 'password', | ||
363 | password: 'bla' | ||
364 | } | ||
365 | |||
366 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
367 | }) | ||
368 | |||
369 | it('Should fail with a too long password', async function () { | ||
370 | const fields = { | ||
371 | currentPassword: 'password', | ||
372 | password: 'super'.repeat(61) | ||
373 | } | ||
374 | |||
375 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
376 | }) | ||
377 | |||
378 | it('Should fail with an non authenticated user', async function () { | ||
379 | const fields = { | ||
380 | videoQuota: 42 | ||
381 | } | ||
382 | |||
383 | await makePutBodyRequest({ | ||
384 | url: server.url, | ||
385 | path: path + userId, | ||
386 | token: 'super token', | ||
387 | fields, | ||
388 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
389 | }) | ||
390 | }) | ||
391 | |||
392 | it('Should fail when updating root role', async function () { | ||
393 | const fields = { | ||
394 | role: UserRole.MODERATOR | ||
395 | } | ||
396 | |||
397 | await makePutBodyRequest({ url: server.url, path: path + rootId, token: server.accessToken, fields }) | ||
398 | }) | ||
399 | |||
400 | it('Should fail with invalid admin flags', async function () { | ||
401 | const fields = { adminFlags: 'toto' } | ||
402 | |||
403 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
404 | }) | ||
405 | |||
406 | it('Should fail to update an admin with a moderator', async function () { | ||
407 | const fields = { | ||
408 | videoQuota: 42 | ||
409 | } | ||
410 | |||
411 | await makePutBodyRequest({ | ||
412 | url: server.url, | ||
413 | path: path + moderatorId, | ||
414 | token: moderatorToken, | ||
415 | fields, | ||
416 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
417 | }) | ||
418 | }) | ||
419 | |||
420 | it('Should succeed to update a user with a moderator', async function () { | ||
421 | const fields = { | ||
422 | videoQuota: 42 | ||
423 | } | ||
424 | |||
425 | await makePutBodyRequest({ | ||
426 | url: server.url, | ||
427 | path: path + userId, | ||
428 | token: moderatorToken, | ||
429 | fields, | ||
430 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
431 | }) | ||
432 | }) | ||
433 | |||
434 | it('Should succeed with the correct params', async function () { | ||
435 | const fields = { | ||
436 | email: 'email@example.com', | ||
437 | emailVerified: true, | ||
438 | videoQuota: 42, | ||
439 | role: UserRole.USER | ||
440 | } | ||
441 | |||
442 | await makePutBodyRequest({ | ||
443 | url: server.url, | ||
444 | path: path + userId, | ||
445 | token: server.accessToken, | ||
446 | fields, | ||
447 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
448 | }) | ||
449 | }) | ||
450 | }) | ||
451 | |||
452 | after(async function () { | ||
453 | MockSmtpServer.Instance.kill() | ||
454 | |||
455 | await cleanupTests([ server ]) | ||
456 | }) | ||
457 | }) | ||
diff --git a/packages/tests/src/api/check-params/users-emails.ts b/packages/tests/src/api/check-params/users-emails.ts new file mode 100644 index 000000000..e382190ec --- /dev/null +++ b/packages/tests/src/api/check-params/users-emails.ts | |||
@@ -0,0 +1,122 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { HttpStatusCode, UserRole } from '@peertube/peertube-models' | ||
3 | import { | ||
4 | cleanupTests, | ||
5 | createSingleServer, | ||
6 | makePostBodyRequest, | ||
7 | PeerTubeServer, | ||
8 | setAccessTokensToServers | ||
9 | } from '@peertube/peertube-server-commands' | ||
10 | |||
11 | describe('Test users API validators', function () { | ||
12 | let server: PeerTubeServer | ||
13 | |||
14 | // --------------------------------------------------------------- | ||
15 | |||
16 | before(async function () { | ||
17 | this.timeout(30000) | ||
18 | |||
19 | server = await createSingleServer(1, { | ||
20 | rates_limit: { | ||
21 | ask_send_email: { | ||
22 | max: 10 | ||
23 | } | ||
24 | } | ||
25 | }) | ||
26 | |||
27 | await setAccessTokensToServers([ server ]) | ||
28 | await server.config.enableSignup(true) | ||
29 | |||
30 | await server.users.generate('moderator2', UserRole.MODERATOR) | ||
31 | |||
32 | await server.registrations.requestRegistration({ | ||
33 | username: 'request1', | ||
34 | registrationReason: 'tt' | ||
35 | }) | ||
36 | }) | ||
37 | |||
38 | describe('When asking a password reset', function () { | ||
39 | const path = '/api/v1/users/ask-reset-password' | ||
40 | |||
41 | it('Should fail with a missing email', async function () { | ||
42 | const fields = {} | ||
43 | |||
44 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
45 | }) | ||
46 | |||
47 | it('Should fail with an invalid email', async function () { | ||
48 | const fields = { email: 'hello' } | ||
49 | |||
50 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
51 | }) | ||
52 | |||
53 | it('Should success with the correct params', async function () { | ||
54 | const fields = { email: 'admin@example.com' } | ||
55 | |||
56 | await makePostBodyRequest({ | ||
57 | url: server.url, | ||
58 | path, | ||
59 | fields, | ||
60 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
61 | }) | ||
62 | }) | ||
63 | }) | ||
64 | |||
65 | describe('When asking for an account verification email', function () { | ||
66 | const path = '/api/v1/users/ask-send-verify-email' | ||
67 | |||
68 | it('Should fail with a missing email', async function () { | ||
69 | const fields = {} | ||
70 | |||
71 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
72 | }) | ||
73 | |||
74 | it('Should fail with an invalid email', async function () { | ||
75 | const fields = { email: 'hello' } | ||
76 | |||
77 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
78 | }) | ||
79 | |||
80 | it('Should succeed with the correct params', async function () { | ||
81 | const fields = { email: 'admin@example.com' } | ||
82 | |||
83 | await makePostBodyRequest({ | ||
84 | url: server.url, | ||
85 | path, | ||
86 | fields, | ||
87 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
88 | }) | ||
89 | }) | ||
90 | }) | ||
91 | |||
92 | describe('When asking for a registration verification email', function () { | ||
93 | const path = '/api/v1/users/registrations/ask-send-verify-email' | ||
94 | |||
95 | it('Should fail with a missing email', async function () { | ||
96 | const fields = {} | ||
97 | |||
98 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
99 | }) | ||
100 | |||
101 | it('Should fail with an invalid email', async function () { | ||
102 | const fields = { email: 'hello' } | ||
103 | |||
104 | await makePostBodyRequest({ url: server.url, path, fields }) | ||
105 | }) | ||
106 | |||
107 | it('Should succeed with the correct params', async function () { | ||
108 | const fields = { email: 'request1@example.com' } | ||
109 | |||
110 | await makePostBodyRequest({ | ||
111 | url: server.url, | ||
112 | path, | ||
113 | fields, | ||
114 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
115 | }) | ||
116 | }) | ||
117 | }) | ||
118 | |||
119 | after(async function () { | ||
120 | await cleanupTests([ server ]) | ||
121 | }) | ||
122 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-blacklist.ts b/packages/tests/src/api/check-params/video-blacklist.ts new file mode 100644 index 000000000..6ec070b9b --- /dev/null +++ b/packages/tests/src/api/check-params/video-blacklist.ts | |||
@@ -0,0 +1,292 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
5 | import { HttpStatusCode, VideoBlacklistType } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | BlacklistCommand, | ||
8 | cleanupTests, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | makePostBodyRequest, | ||
12 | makePutBodyRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | waitJobs | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('Test video blacklist API validators', function () { | ||
19 | let servers: PeerTubeServer[] | ||
20 | let notBlacklistedVideoId: string | ||
21 | let remoteVideoUUID: string | ||
22 | let userAccessToken1 = '' | ||
23 | let userAccessToken2 = '' | ||
24 | let command: BlacklistCommand | ||
25 | |||
26 | // --------------------------------------------------------------- | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(120000) | ||
30 | |||
31 | servers = await createMultipleServers(2) | ||
32 | |||
33 | await setAccessTokensToServers(servers) | ||
34 | await doubleFollow(servers[0], servers[1]) | ||
35 | |||
36 | { | ||
37 | const username = 'user1' | ||
38 | const password = 'my super password' | ||
39 | await servers[0].users.create({ username, password }) | ||
40 | userAccessToken1 = await servers[0].login.getAccessToken({ username, password }) | ||
41 | } | ||
42 | |||
43 | { | ||
44 | const username = 'user2' | ||
45 | const password = 'my super password' | ||
46 | await servers[0].users.create({ username, password }) | ||
47 | userAccessToken2 = await servers[0].login.getAccessToken({ username, password }) | ||
48 | } | ||
49 | |||
50 | { | ||
51 | servers[0].store.videoCreated = await servers[0].videos.upload({ token: userAccessToken1 }) | ||
52 | } | ||
53 | |||
54 | { | ||
55 | const { uuid } = await servers[0].videos.upload() | ||
56 | notBlacklistedVideoId = uuid | ||
57 | } | ||
58 | |||
59 | { | ||
60 | const { uuid } = await servers[1].videos.upload() | ||
61 | remoteVideoUUID = uuid | ||
62 | } | ||
63 | |||
64 | await waitJobs(servers) | ||
65 | |||
66 | command = servers[0].blacklist | ||
67 | }) | ||
68 | |||
69 | describe('When adding a video in blacklist', function () { | ||
70 | const basePath = '/api/v1/videos/' | ||
71 | |||
72 | it('Should fail with nothing', async function () { | ||
73 | const path = basePath + servers[0].store.videoCreated + '/blacklist' | ||
74 | const fields = {} | ||
75 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) | ||
76 | }) | ||
77 | |||
78 | it('Should fail with a wrong video', async function () { | ||
79 | const wrongPath = '/api/v1/videos/blabla/blacklist' | ||
80 | const fields = {} | ||
81 | await makePostBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields }) | ||
82 | }) | ||
83 | |||
84 | it('Should fail with a non authenticated user', async function () { | ||
85 | const path = basePath + servers[0].store.videoCreated + '/blacklist' | ||
86 | const fields = {} | ||
87 | await makePostBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
88 | }) | ||
89 | |||
90 | it('Should fail with a non admin user', async function () { | ||
91 | const path = basePath + servers[0].store.videoCreated + '/blacklist' | ||
92 | const fields = {} | ||
93 | await makePostBodyRequest({ | ||
94 | url: servers[0].url, | ||
95 | path, | ||
96 | token: userAccessToken2, | ||
97 | fields, | ||
98 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
99 | }) | ||
100 | }) | ||
101 | |||
102 | it('Should fail with an invalid reason', async function () { | ||
103 | const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist' | ||
104 | const fields = { reason: 'a'.repeat(305) } | ||
105 | |||
106 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) | ||
107 | }) | ||
108 | |||
109 | it('Should fail to unfederate a remote video', async function () { | ||
110 | const path = basePath + remoteVideoUUID + '/blacklist' | ||
111 | const fields = { unfederate: true } | ||
112 | |||
113 | await makePostBodyRequest({ | ||
114 | url: servers[0].url, | ||
115 | path, | ||
116 | token: servers[0].accessToken, | ||
117 | fields, | ||
118 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
119 | }) | ||
120 | }) | ||
121 | |||
122 | it('Should succeed with the correct params', async function () { | ||
123 | const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist' | ||
124 | const fields = {} | ||
125 | |||
126 | await makePostBodyRequest({ | ||
127 | url: servers[0].url, | ||
128 | path, | ||
129 | token: servers[0].accessToken, | ||
130 | fields, | ||
131 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
132 | }) | ||
133 | }) | ||
134 | }) | ||
135 | |||
136 | describe('When updating a video in blacklist', function () { | ||
137 | const basePath = '/api/v1/videos/' | ||
138 | |||
139 | it('Should fail with a wrong video', async function () { | ||
140 | const wrongPath = '/api/v1/videos/blabla/blacklist' | ||
141 | const fields = {} | ||
142 | await makePutBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields }) | ||
143 | }) | ||
144 | |||
145 | it('Should fail with a video not blacklisted', async function () { | ||
146 | const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist' | ||
147 | const fields = {} | ||
148 | await makePutBodyRequest({ | ||
149 | url: servers[0].url, | ||
150 | path, | ||
151 | token: servers[0].accessToken, | ||
152 | fields, | ||
153 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
154 | }) | ||
155 | }) | ||
156 | |||
157 | it('Should fail with a non authenticated user', async function () { | ||
158 | const path = basePath + servers[0].store.videoCreated + '/blacklist' | ||
159 | const fields = {} | ||
160 | await makePutBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
161 | }) | ||
162 | |||
163 | it('Should fail with a non admin user', async function () { | ||
164 | const path = basePath + servers[0].store.videoCreated + '/blacklist' | ||
165 | const fields = {} | ||
166 | await makePutBodyRequest({ | ||
167 | url: servers[0].url, | ||
168 | path, | ||
169 | token: userAccessToken2, | ||
170 | fields, | ||
171 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
172 | }) | ||
173 | }) | ||
174 | |||
175 | it('Should fail with an invalid reason', async function () { | ||
176 | const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist' | ||
177 | const fields = { reason: 'a'.repeat(305) } | ||
178 | |||
179 | await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) | ||
180 | }) | ||
181 | |||
182 | it('Should succeed with the correct params', async function () { | ||
183 | const path = basePath + servers[0].store.videoCreated.shortUUID + '/blacklist' | ||
184 | const fields = { reason: 'hello' } | ||
185 | |||
186 | await makePutBodyRequest({ | ||
187 | url: servers[0].url, | ||
188 | path, | ||
189 | token: servers[0].accessToken, | ||
190 | fields, | ||
191 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
192 | }) | ||
193 | }) | ||
194 | }) | ||
195 | |||
196 | describe('When getting blacklisted video', function () { | ||
197 | |||
198 | it('Should fail with a non authenticated user', async function () { | ||
199 | await servers[0].videos.get({ id: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
200 | }) | ||
201 | |||
202 | it('Should fail with another user', async function () { | ||
203 | await servers[0].videos.getWithToken({ | ||
204 | token: userAccessToken2, | ||
205 | id: servers[0].store.videoCreated.uuid, | ||
206 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
207 | }) | ||
208 | }) | ||
209 | |||
210 | it('Should succeed with the owner authenticated user', async function () { | ||
211 | const video = await servers[0].videos.getWithToken({ token: userAccessToken1, id: servers[0].store.videoCreated.uuid }) | ||
212 | expect(video.blacklisted).to.be.true | ||
213 | }) | ||
214 | |||
215 | it('Should succeed with an admin', async function () { | ||
216 | const video = servers[0].store.videoCreated | ||
217 | |||
218 | for (const id of [ video.id, video.uuid, video.shortUUID ]) { | ||
219 | const video = await servers[0].videos.getWithToken({ id, expectedStatus: HttpStatusCode.OK_200 }) | ||
220 | expect(video.blacklisted).to.be.true | ||
221 | } | ||
222 | }) | ||
223 | }) | ||
224 | |||
225 | describe('When removing a video in blacklist', function () { | ||
226 | |||
227 | it('Should fail with a non authenticated user', async function () { | ||
228 | await command.remove({ | ||
229 | token: 'fake token', | ||
230 | videoId: servers[0].store.videoCreated.uuid, | ||
231 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
232 | }) | ||
233 | }) | ||
234 | |||
235 | it('Should fail with a non admin user', async function () { | ||
236 | await command.remove({ | ||
237 | token: userAccessToken2, | ||
238 | videoId: servers[0].store.videoCreated.uuid, | ||
239 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
240 | }) | ||
241 | }) | ||
242 | |||
243 | it('Should fail with an incorrect id', async function () { | ||
244 | await command.remove({ videoId: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
245 | }) | ||
246 | |||
247 | it('Should fail with a not blacklisted video', async function () { | ||
248 | // The video was not added to the blacklist so it should fail | ||
249 | await command.remove({ videoId: notBlacklistedVideoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
250 | }) | ||
251 | |||
252 | it('Should succeed with the correct params', async function () { | ||
253 | await command.remove({ videoId: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
254 | }) | ||
255 | }) | ||
256 | |||
257 | describe('When listing videos in blacklist', function () { | ||
258 | const basePath = '/api/v1/videos/blacklist/' | ||
259 | |||
260 | it('Should fail with a non authenticated user', async function () { | ||
261 | await servers[0].blacklist.list({ token: 'fake token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
262 | }) | ||
263 | |||
264 | it('Should fail with a non admin user', async function () { | ||
265 | await servers[0].blacklist.list({ token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
266 | }) | ||
267 | |||
268 | it('Should fail with a bad start pagination', async function () { | ||
269 | await checkBadStartPagination(servers[0].url, basePath, servers[0].accessToken) | ||
270 | }) | ||
271 | |||
272 | it('Should fail with a bad count pagination', async function () { | ||
273 | await checkBadCountPagination(servers[0].url, basePath, servers[0].accessToken) | ||
274 | }) | ||
275 | |||
276 | it('Should fail with an incorrect sort', async function () { | ||
277 | await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken) | ||
278 | }) | ||
279 | |||
280 | it('Should fail with an invalid type', async function () { | ||
281 | await servers[0].blacklist.list({ type: 0 as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
282 | }) | ||
283 | |||
284 | it('Should succeed with the correct parameters', async function () { | ||
285 | await servers[0].blacklist.list({ type: VideoBlacklistType.MANUAL }) | ||
286 | }) | ||
287 | }) | ||
288 | |||
289 | after(async function () { | ||
290 | await cleanupTests(servers) | ||
291 | }) | ||
292 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-captions.ts b/packages/tests/src/api/check-params/video-captions.ts new file mode 100644 index 000000000..4150b095f --- /dev/null +++ b/packages/tests/src/api/check-params/video-captions.ts | |||
@@ -0,0 +1,307 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
4 | import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeDeleteRequest, | ||
9 | makeGetRequest, | ||
10 | makeUploadRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test video captions API validator', function () { | ||
16 | const path = '/api/v1/videos/' | ||
17 | |||
18 | let server: PeerTubeServer | ||
19 | let userAccessToken: string | ||
20 | let video: VideoCreateResult | ||
21 | let privateVideo: VideoCreateResult | ||
22 | |||
23 | // --------------------------------------------------------------- | ||
24 | |||
25 | before(async function () { | ||
26 | this.timeout(120000) | ||
27 | |||
28 | server = await createSingleServer(1) | ||
29 | |||
30 | await setAccessTokensToServers([ server ]) | ||
31 | |||
32 | video = await server.videos.upload() | ||
33 | privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } }) | ||
34 | |||
35 | { | ||
36 | const user = { | ||
37 | username: 'user1', | ||
38 | password: 'my super password' | ||
39 | } | ||
40 | await server.users.create({ username: user.username, password: user.password }) | ||
41 | userAccessToken = await server.login.getAccessToken(user) | ||
42 | } | ||
43 | }) | ||
44 | |||
45 | describe('When adding video caption', function () { | ||
46 | const fields = { } | ||
47 | const attaches = { | ||
48 | captionfile: buildAbsoluteFixturePath('subtitle-good1.vtt') | ||
49 | } | ||
50 | |||
51 | it('Should fail without a valid uuid', async function () { | ||
52 | await makeUploadRequest({ | ||
53 | method: 'PUT', | ||
54 | url: server.url, | ||
55 | path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions/fr', | ||
56 | token: server.accessToken, | ||
57 | fields, | ||
58 | attaches | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | it('Should fail with an unknown id', async function () { | ||
63 | await makeUploadRequest({ | ||
64 | method: 'PUT', | ||
65 | url: server.url, | ||
66 | path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr', | ||
67 | token: server.accessToken, | ||
68 | fields, | ||
69 | attaches, | ||
70 | expectedStatus: 404 | ||
71 | }) | ||
72 | }) | ||
73 | |||
74 | it('Should fail with a missing language in path', async function () { | ||
75 | const captionPath = path + video.uuid + '/captions' | ||
76 | await makeUploadRequest({ | ||
77 | method: 'PUT', | ||
78 | url: server.url, | ||
79 | path: captionPath, | ||
80 | token: server.accessToken, | ||
81 | fields, | ||
82 | attaches | ||
83 | }) | ||
84 | }) | ||
85 | |||
86 | it('Should fail with an unknown language', async function () { | ||
87 | const captionPath = path + video.uuid + '/captions/15' | ||
88 | await makeUploadRequest({ | ||
89 | method: 'PUT', | ||
90 | url: server.url, | ||
91 | path: captionPath, | ||
92 | token: server.accessToken, | ||
93 | fields, | ||
94 | attaches | ||
95 | }) | ||
96 | }) | ||
97 | |||
98 | it('Should fail without access token', async function () { | ||
99 | const captionPath = path + video.uuid + '/captions/fr' | ||
100 | await makeUploadRequest({ | ||
101 | method: 'PUT', | ||
102 | url: server.url, | ||
103 | path: captionPath, | ||
104 | fields, | ||
105 | attaches, | ||
106 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
107 | }) | ||
108 | }) | ||
109 | |||
110 | it('Should fail with a bad access token', async function () { | ||
111 | const captionPath = path + video.uuid + '/captions/fr' | ||
112 | await makeUploadRequest({ | ||
113 | method: 'PUT', | ||
114 | url: server.url, | ||
115 | path: captionPath, | ||
116 | token: 'blabla', | ||
117 | fields, | ||
118 | attaches, | ||
119 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
120 | }) | ||
121 | }) | ||
122 | |||
123 | // We accept any file now | ||
124 | // it('Should fail with an invalid captionfile extension', async function () { | ||
125 | // const attaches = { | ||
126 | // 'captionfile': buildAbsoluteFixturePath('subtitle-bad.txt') | ||
127 | // } | ||
128 | // | ||
129 | // const captionPath = path + video.uuid + '/captions/fr' | ||
130 | // await makeUploadRequest({ | ||
131 | // method: 'PUT', | ||
132 | // url: server.url, | ||
133 | // path: captionPath, | ||
134 | // token: server.accessToken, | ||
135 | // fields, | ||
136 | // attaches, | ||
137 | // expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
138 | // }) | ||
139 | // }) | ||
140 | |||
141 | // We don't check the extension yet | ||
142 | // it('Should fail with an invalid captionfile extension and octet-stream mime type', async function () { | ||
143 | // await createVideoCaption({ | ||
144 | // url: server.url, | ||
145 | // accessToken: server.accessToken, | ||
146 | // language: 'zh', | ||
147 | // videoId: video.uuid, | ||
148 | // fixture: 'subtitle-bad.txt', | ||
149 | // mimeType: 'application/octet-stream', | ||
150 | // expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
151 | // }) | ||
152 | // }) | ||
153 | |||
154 | it('Should succeed with a valid captionfile extension and octet-stream mime type', async function () { | ||
155 | await server.captions.add({ | ||
156 | language: 'zh', | ||
157 | videoId: video.uuid, | ||
158 | fixture: 'subtitle-good.srt', | ||
159 | mimeType: 'application/octet-stream' | ||
160 | }) | ||
161 | }) | ||
162 | |||
163 | // We don't check the file validity yet | ||
164 | // it('Should fail with an invalid captionfile srt', async function () { | ||
165 | // const attaches = { | ||
166 | // 'captionfile': buildAbsoluteFixturePath('subtitle-bad.srt') | ||
167 | // } | ||
168 | // | ||
169 | // const captionPath = path + video.uuid + '/captions/fr' | ||
170 | // await makeUploadRequest({ | ||
171 | // method: 'PUT', | ||
172 | // url: server.url, | ||
173 | // path: captionPath, | ||
174 | // token: server.accessToken, | ||
175 | // fields, | ||
176 | // attaches, | ||
177 | // expectedStatus: HttpStatusCode.INTERNAL_SERVER_ERROR_500 | ||
178 | // }) | ||
179 | // }) | ||
180 | |||
181 | it('Should success with the correct parameters', async function () { | ||
182 | const captionPath = path + video.uuid + '/captions/fr' | ||
183 | await makeUploadRequest({ | ||
184 | method: 'PUT', | ||
185 | url: server.url, | ||
186 | path: captionPath, | ||
187 | token: server.accessToken, | ||
188 | fields, | ||
189 | attaches, | ||
190 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
191 | }) | ||
192 | }) | ||
193 | }) | ||
194 | |||
195 | describe('When listing video captions', function () { | ||
196 | it('Should fail without a valid uuid', async function () { | ||
197 | await makeGetRequest({ url: server.url, path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions' }) | ||
198 | }) | ||
199 | |||
200 | it('Should fail with an unknown id', async function () { | ||
201 | await makeGetRequest({ | ||
202 | url: server.url, | ||
203 | path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions', | ||
204 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
205 | }) | ||
206 | }) | ||
207 | |||
208 | it('Should fail with a private video without token', async function () { | ||
209 | await makeGetRequest({ | ||
210 | url: server.url, | ||
211 | path: path + privateVideo.shortUUID + '/captions', | ||
212 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
213 | }) | ||
214 | }) | ||
215 | |||
216 | it('Should fail with another user token', async function () { | ||
217 | await makeGetRequest({ | ||
218 | url: server.url, | ||
219 | token: userAccessToken, | ||
220 | path: path + privateVideo.shortUUID + '/captions', | ||
221 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
222 | }) | ||
223 | }) | ||
224 | |||
225 | it('Should success with the correct parameters', async function () { | ||
226 | await makeGetRequest({ url: server.url, path: path + video.shortUUID + '/captions', expectedStatus: HttpStatusCode.OK_200 }) | ||
227 | |||
228 | await makeGetRequest({ | ||
229 | url: server.url, | ||
230 | path: path + privateVideo.shortUUID + '/captions', | ||
231 | token: server.accessToken, | ||
232 | expectedStatus: HttpStatusCode.OK_200 | ||
233 | }) | ||
234 | }) | ||
235 | }) | ||
236 | |||
237 | describe('When deleting video caption', function () { | ||
238 | it('Should fail without a valid uuid', async function () { | ||
239 | await makeDeleteRequest({ | ||
240 | url: server.url, | ||
241 | path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions/fr', | ||
242 | token: server.accessToken | ||
243 | }) | ||
244 | }) | ||
245 | |||
246 | it('Should fail with an unknown id', async function () { | ||
247 | await makeDeleteRequest({ | ||
248 | url: server.url, | ||
249 | path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr', | ||
250 | token: server.accessToken, | ||
251 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
252 | }) | ||
253 | }) | ||
254 | |||
255 | it('Should fail with an invalid language', async function () { | ||
256 | await makeDeleteRequest({ | ||
257 | url: server.url, | ||
258 | path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/16', | ||
259 | token: server.accessToken | ||
260 | }) | ||
261 | }) | ||
262 | |||
263 | it('Should fail with a missing language', async function () { | ||
264 | const captionPath = path + video.shortUUID + '/captions' | ||
265 | await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken }) | ||
266 | }) | ||
267 | |||
268 | it('Should fail with an unknown language', async function () { | ||
269 | const captionPath = path + video.shortUUID + '/captions/15' | ||
270 | await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken }) | ||
271 | }) | ||
272 | |||
273 | it('Should fail without access token', async function () { | ||
274 | const captionPath = path + video.shortUUID + '/captions/fr' | ||
275 | await makeDeleteRequest({ url: server.url, path: captionPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
276 | }) | ||
277 | |||
278 | it('Should fail with a bad access token', async function () { | ||
279 | const captionPath = path + video.shortUUID + '/captions/fr' | ||
280 | await makeDeleteRequest({ url: server.url, path: captionPath, token: 'coucou', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
281 | }) | ||
282 | |||
283 | it('Should fail with another user', async function () { | ||
284 | const captionPath = path + video.shortUUID + '/captions/fr' | ||
285 | await makeDeleteRequest({ | ||
286 | url: server.url, | ||
287 | path: captionPath, | ||
288 | token: userAccessToken, | ||
289 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
290 | }) | ||
291 | }) | ||
292 | |||
293 | it('Should success with the correct parameters', async function () { | ||
294 | const captionPath = path + video.shortUUID + '/captions/fr' | ||
295 | await makeDeleteRequest({ | ||
296 | url: server.url, | ||
297 | path: captionPath, | ||
298 | token: server.accessToken, | ||
299 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
300 | }) | ||
301 | }) | ||
302 | }) | ||
303 | |||
304 | after(async function () { | ||
305 | await cleanupTests([ server ]) | ||
306 | }) | ||
307 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-channel-syncs.ts b/packages/tests/src/api/check-params/video-channel-syncs.ts new file mode 100644 index 000000000..d95f3319a --- /dev/null +++ b/packages/tests/src/api/check-params/video-channel-syncs.ts | |||
@@ -0,0 +1,319 @@ | |||
1 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
2 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
3 | import { HttpStatusCode, VideoChannelSyncCreate } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | ChannelSyncsCommand, | ||
6 | createSingleServer, | ||
7 | makePostBodyRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test video channel sync API validator', () => { | ||
14 | const path = '/api/v1/video-channel-syncs' | ||
15 | let server: PeerTubeServer | ||
16 | let command: ChannelSyncsCommand | ||
17 | let rootChannelId: number | ||
18 | let rootChannelSyncId: number | ||
19 | const userInfo = { | ||
20 | accessToken: '', | ||
21 | username: 'user1', | ||
22 | id: -1, | ||
23 | channelId: -1, | ||
24 | syncId: -1 | ||
25 | } | ||
26 | |||
27 | async function withChannelSyncDisabled<T> (callback: () => Promise<T>): Promise<void> { | ||
28 | try { | ||
29 | await server.config.disableChannelSync() | ||
30 | await callback() | ||
31 | } finally { | ||
32 | await server.config.enableChannelSync() | ||
33 | } | ||
34 | } | ||
35 | |||
36 | async function withMaxSyncsPerUser<T> (maxSync: number, callback: () => Promise<T>): Promise<void> { | ||
37 | const origConfig = await server.config.getCustomConfig() | ||
38 | |||
39 | await server.config.updateExistingSubConfig({ | ||
40 | newConfig: { | ||
41 | import: { | ||
42 | videoChannelSynchronization: { | ||
43 | maxPerUser: maxSync | ||
44 | } | ||
45 | } | ||
46 | } | ||
47 | }) | ||
48 | |||
49 | try { | ||
50 | await callback() | ||
51 | } finally { | ||
52 | await server.config.updateCustomConfig({ newCustomConfig: origConfig }) | ||
53 | } | ||
54 | } | ||
55 | |||
56 | before(async function () { | ||
57 | this.timeout(30_000) | ||
58 | |||
59 | server = await createSingleServer(1) | ||
60 | |||
61 | await setAccessTokensToServers([ server ]) | ||
62 | await setDefaultVideoChannel([ server ]) | ||
63 | |||
64 | command = server.channelSyncs | ||
65 | |||
66 | rootChannelId = server.store.channel.id | ||
67 | |||
68 | { | ||
69 | userInfo.accessToken = await server.users.generateUserAndToken(userInfo.username) | ||
70 | |||
71 | const { videoChannels, id: userId } = await server.users.getMyInfo({ token: userInfo.accessToken }) | ||
72 | userInfo.id = userId | ||
73 | userInfo.channelId = videoChannels[0].id | ||
74 | } | ||
75 | |||
76 | await server.config.enableChannelSync() | ||
77 | }) | ||
78 | |||
79 | describe('When creating a sync', function () { | ||
80 | let baseCorrectParams: VideoChannelSyncCreate | ||
81 | |||
82 | before(function () { | ||
83 | baseCorrectParams = { | ||
84 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
85 | videoChannelId: rootChannelId | ||
86 | } | ||
87 | }) | ||
88 | |||
89 | it('Should fail when sync is disabled', async function () { | ||
90 | await withChannelSyncDisabled(async () => { | ||
91 | await command.create({ | ||
92 | token: server.accessToken, | ||
93 | attributes: baseCorrectParams, | ||
94 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
95 | }) | ||
96 | }) | ||
97 | }) | ||
98 | |||
99 | it('Should fail with nothing', async function () { | ||
100 | const fields = {} | ||
101 | await makePostBodyRequest({ | ||
102 | url: server.url, | ||
103 | path, | ||
104 | token: server.accessToken, | ||
105 | fields, | ||
106 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
107 | }) | ||
108 | }) | ||
109 | |||
110 | it('Should fail with no authentication', async function () { | ||
111 | await command.create({ | ||
112 | token: null, | ||
113 | attributes: baseCorrectParams, | ||
114 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
115 | }) | ||
116 | }) | ||
117 | |||
118 | it('Should fail without a target url', async function () { | ||
119 | const attributes: VideoChannelSyncCreate = { | ||
120 | ...baseCorrectParams, | ||
121 | externalChannelUrl: null | ||
122 | } | ||
123 | await command.create({ | ||
124 | token: server.accessToken, | ||
125 | attributes, | ||
126 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
127 | }) | ||
128 | }) | ||
129 | |||
130 | it('Should fail without a channelId', async function () { | ||
131 | const attributes: VideoChannelSyncCreate = { | ||
132 | ...baseCorrectParams, | ||
133 | videoChannelId: null | ||
134 | } | ||
135 | await command.create({ | ||
136 | token: server.accessToken, | ||
137 | attributes, | ||
138 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
139 | }) | ||
140 | }) | ||
141 | |||
142 | it('Should fail with a channelId refering nothing', async function () { | ||
143 | const attributes: VideoChannelSyncCreate = { | ||
144 | ...baseCorrectParams, | ||
145 | videoChannelId: 42 | ||
146 | } | ||
147 | await command.create({ | ||
148 | token: server.accessToken, | ||
149 | attributes, | ||
150 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
151 | }) | ||
152 | }) | ||
153 | |||
154 | it('Should fail to create a sync when the user does not own the channel', async function () { | ||
155 | await command.create({ | ||
156 | token: userInfo.accessToken, | ||
157 | attributes: baseCorrectParams, | ||
158 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
159 | }) | ||
160 | }) | ||
161 | |||
162 | it('Should succeed to create a sync with root and for another user\'s channel', async function () { | ||
163 | const { videoChannelSync } = await command.create({ | ||
164 | token: server.accessToken, | ||
165 | attributes: { | ||
166 | ...baseCorrectParams, | ||
167 | videoChannelId: userInfo.channelId | ||
168 | }, | ||
169 | expectedStatus: HttpStatusCode.OK_200 | ||
170 | }) | ||
171 | userInfo.syncId = videoChannelSync.id | ||
172 | }) | ||
173 | |||
174 | it('Should succeed with the correct parameters', async function () { | ||
175 | const { videoChannelSync } = await command.create({ | ||
176 | token: server.accessToken, | ||
177 | attributes: baseCorrectParams, | ||
178 | expectedStatus: HttpStatusCode.OK_200 | ||
179 | }) | ||
180 | rootChannelSyncId = videoChannelSync.id | ||
181 | }) | ||
182 | |||
183 | it('Should fail when the user exceeds allowed number of synchronizations', async function () { | ||
184 | await withMaxSyncsPerUser(1, async () => { | ||
185 | await command.create({ | ||
186 | token: server.accessToken, | ||
187 | attributes: { | ||
188 | ...baseCorrectParams, | ||
189 | videoChannelId: userInfo.channelId | ||
190 | }, | ||
191 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
192 | }) | ||
193 | }) | ||
194 | }) | ||
195 | }) | ||
196 | |||
197 | describe('When listing my channel syncs', function () { | ||
198 | const myPath = '/api/v1/accounts/root/video-channel-syncs' | ||
199 | |||
200 | it('Should fail with a bad start pagination', async function () { | ||
201 | await checkBadStartPagination(server.url, myPath, server.accessToken) | ||
202 | }) | ||
203 | |||
204 | it('Should fail with a bad count pagination', async function () { | ||
205 | await checkBadCountPagination(server.url, myPath, server.accessToken) | ||
206 | }) | ||
207 | |||
208 | it('Should fail with an incorrect sort', async function () { | ||
209 | await checkBadSortPagination(server.url, myPath, server.accessToken) | ||
210 | }) | ||
211 | |||
212 | it('Should succeed with the correct parameters', async function () { | ||
213 | await command.listByAccount({ | ||
214 | accountName: 'root', | ||
215 | token: server.accessToken, | ||
216 | expectedStatus: HttpStatusCode.OK_200 | ||
217 | }) | ||
218 | }) | ||
219 | |||
220 | it('Should fail with no authentication', async function () { | ||
221 | await command.listByAccount({ | ||
222 | accountName: 'root', | ||
223 | token: null, | ||
224 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
225 | }) | ||
226 | }) | ||
227 | |||
228 | it('Should fail when a simple user lists another user\'s synchronizations', async function () { | ||
229 | await command.listByAccount({ | ||
230 | accountName: 'root', | ||
231 | token: userInfo.accessToken, | ||
232 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
233 | }) | ||
234 | }) | ||
235 | |||
236 | it('Should succeed when root lists another user\'s synchronizations', async function () { | ||
237 | await command.listByAccount({ | ||
238 | accountName: userInfo.username, | ||
239 | token: server.accessToken, | ||
240 | expectedStatus: HttpStatusCode.OK_200 | ||
241 | }) | ||
242 | }) | ||
243 | |||
244 | it('Should succeed even with synchronization disabled', async function () { | ||
245 | await withChannelSyncDisabled(async function () { | ||
246 | await command.listByAccount({ | ||
247 | accountName: 'root', | ||
248 | token: server.accessToken, | ||
249 | expectedStatus: HttpStatusCode.OK_200 | ||
250 | }) | ||
251 | }) | ||
252 | }) | ||
253 | }) | ||
254 | |||
255 | describe('When triggering deletion', function () { | ||
256 | it('should fail with no authentication', async function () { | ||
257 | await command.delete({ | ||
258 | channelSyncId: userInfo.syncId, | ||
259 | token: null, | ||
260 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
261 | }) | ||
262 | }) | ||
263 | |||
264 | it('Should fail when channelSyncId does not refer to any sync', async function () { | ||
265 | await command.delete({ | ||
266 | channelSyncId: 42, | ||
267 | token: server.accessToken, | ||
268 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
269 | }) | ||
270 | }) | ||
271 | |||
272 | it('Should fail when sync is not owned by the user', async function () { | ||
273 | await command.delete({ | ||
274 | channelSyncId: rootChannelSyncId, | ||
275 | token: userInfo.accessToken, | ||
276 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
277 | }) | ||
278 | }) | ||
279 | |||
280 | it('Should succeed when root delete a sync they do not own', async function () { | ||
281 | await command.delete({ | ||
282 | channelSyncId: userInfo.syncId, | ||
283 | token: server.accessToken, | ||
284 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
285 | }) | ||
286 | }) | ||
287 | |||
288 | it('should succeed when user delete a sync they own', async function () { | ||
289 | const { videoChannelSync } = await command.create({ | ||
290 | attributes: { | ||
291 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
292 | videoChannelId: userInfo.channelId | ||
293 | }, | ||
294 | token: server.accessToken, | ||
295 | expectedStatus: HttpStatusCode.OK_200 | ||
296 | }) | ||
297 | |||
298 | await command.delete({ | ||
299 | channelSyncId: videoChannelSync.id, | ||
300 | token: server.accessToken, | ||
301 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
302 | }) | ||
303 | }) | ||
304 | |||
305 | it('Should succeed even when synchronization is disabled', async function () { | ||
306 | await withChannelSyncDisabled(async function () { | ||
307 | await command.delete({ | ||
308 | channelSyncId: rootChannelSyncId, | ||
309 | token: server.accessToken, | ||
310 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
311 | }) | ||
312 | }) | ||
313 | }) | ||
314 | }) | ||
315 | |||
316 | after(async function () { | ||
317 | await server?.kill() | ||
318 | }) | ||
319 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-channels.ts b/packages/tests/src/api/check-params/video-channels.ts new file mode 100644 index 000000000..84b962b19 --- /dev/null +++ b/packages/tests/src/api/check-params/video-channels.ts | |||
@@ -0,0 +1,379 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { omit } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models' | ||
6 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | ChannelsCommand, | ||
10 | cleanupTests, | ||
11 | createSingleServer, | ||
12 | makeGetRequest, | ||
13 | makePostBodyRequest, | ||
14 | makePutBodyRequest, | ||
15 | makeUploadRequest, | ||
16 | PeerTubeServer, | ||
17 | setAccessTokensToServers | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | |||
20 | describe('Test video channels API validator', function () { | ||
21 | const videoChannelPath = '/api/v1/video-channels' | ||
22 | let server: PeerTubeServer | ||
23 | const userInfo = { | ||
24 | accessToken: '', | ||
25 | channelName: 'fake_channel', | ||
26 | id: -1, | ||
27 | videoQuota: -1, | ||
28 | videoQuotaDaily: -1 | ||
29 | } | ||
30 | let command: ChannelsCommand | ||
31 | |||
32 | // --------------------------------------------------------------- | ||
33 | |||
34 | before(async function () { | ||
35 | this.timeout(30000) | ||
36 | |||
37 | server = await createSingleServer(1) | ||
38 | |||
39 | await setAccessTokensToServers([ server ]) | ||
40 | |||
41 | const userCreds = { | ||
42 | username: 'fake', | ||
43 | password: 'fake_password' | ||
44 | } | ||
45 | |||
46 | { | ||
47 | const user = await server.users.create({ username: userCreds.username, password: userCreds.password }) | ||
48 | userInfo.id = user.id | ||
49 | userInfo.accessToken = await server.login.getAccessToken(userCreds) | ||
50 | } | ||
51 | |||
52 | command = server.channels | ||
53 | }) | ||
54 | |||
55 | describe('When listing a video channels', function () { | ||
56 | it('Should fail with a bad start pagination', async function () { | ||
57 | await checkBadStartPagination(server.url, videoChannelPath, server.accessToken) | ||
58 | }) | ||
59 | |||
60 | it('Should fail with a bad count pagination', async function () { | ||
61 | await checkBadCountPagination(server.url, videoChannelPath, server.accessToken) | ||
62 | }) | ||
63 | |||
64 | it('Should fail with an incorrect sort', async function () { | ||
65 | await checkBadSortPagination(server.url, videoChannelPath, server.accessToken) | ||
66 | }) | ||
67 | }) | ||
68 | |||
69 | describe('When listing account video channels', function () { | ||
70 | const accountChannelPath = '/api/v1/accounts/fake/video-channels' | ||
71 | |||
72 | it('Should fail with a bad start pagination', async function () { | ||
73 | await checkBadStartPagination(server.url, accountChannelPath, server.accessToken) | ||
74 | }) | ||
75 | |||
76 | it('Should fail with a bad count pagination', async function () { | ||
77 | await checkBadCountPagination(server.url, accountChannelPath, server.accessToken) | ||
78 | }) | ||
79 | |||
80 | it('Should fail with an incorrect sort', async function () { | ||
81 | await checkBadSortPagination(server.url, accountChannelPath, server.accessToken) | ||
82 | }) | ||
83 | |||
84 | it('Should fail with a unknown account', async function () { | ||
85 | await server.channels.listByAccount({ accountName: 'unknown', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
86 | }) | ||
87 | |||
88 | it('Should succeed with the correct parameters', async function () { | ||
89 | await makeGetRequest({ | ||
90 | url: server.url, | ||
91 | path: accountChannelPath, | ||
92 | expectedStatus: HttpStatusCode.OK_200 | ||
93 | }) | ||
94 | }) | ||
95 | }) | ||
96 | |||
97 | describe('When adding a video channel', function () { | ||
98 | const baseCorrectParams = { | ||
99 | name: 'super_channel', | ||
100 | displayName: 'hello', | ||
101 | description: 'super description', | ||
102 | support: 'super support text' | ||
103 | } | ||
104 | |||
105 | it('Should fail with a non authenticated user', async function () { | ||
106 | await makePostBodyRequest({ | ||
107 | url: server.url, | ||
108 | path: videoChannelPath, | ||
109 | token: 'none', | ||
110 | fields: baseCorrectParams, | ||
111 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
112 | }) | ||
113 | }) | ||
114 | |||
115 | it('Should fail with nothing', async function () { | ||
116 | const fields = {} | ||
117 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | ||
118 | }) | ||
119 | |||
120 | it('Should fail without a name', async function () { | ||
121 | const fields = omit(baseCorrectParams, [ 'name' ]) | ||
122 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | ||
123 | }) | ||
124 | |||
125 | it('Should fail with a bad name', async function () { | ||
126 | const fields = { ...baseCorrectParams, name: 'super name' } | ||
127 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | ||
128 | }) | ||
129 | |||
130 | it('Should fail without a name', async function () { | ||
131 | const fields = omit(baseCorrectParams, [ 'displayName' ]) | ||
132 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | ||
133 | }) | ||
134 | |||
135 | it('Should fail with a long name', async function () { | ||
136 | const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) } | ||
137 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | ||
138 | }) | ||
139 | |||
140 | it('Should fail with a long description', async function () { | ||
141 | const fields = { ...baseCorrectParams, description: 'super'.repeat(201) } | ||
142 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | ||
143 | }) | ||
144 | |||
145 | it('Should fail with a long support text', async function () { | ||
146 | const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } | ||
147 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | ||
148 | }) | ||
149 | |||
150 | it('Should succeed with the correct parameters', async function () { | ||
151 | await makePostBodyRequest({ | ||
152 | url: server.url, | ||
153 | path: videoChannelPath, | ||
154 | token: server.accessToken, | ||
155 | fields: baseCorrectParams, | ||
156 | expectedStatus: HttpStatusCode.OK_200 | ||
157 | }) | ||
158 | }) | ||
159 | |||
160 | it('Should fail when adding a channel with the same username', async function () { | ||
161 | await makePostBodyRequest({ | ||
162 | url: server.url, | ||
163 | path: videoChannelPath, | ||
164 | token: server.accessToken, | ||
165 | fields: baseCorrectParams, | ||
166 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
167 | }) | ||
168 | }) | ||
169 | }) | ||
170 | |||
171 | describe('When updating a video channel', function () { | ||
172 | const baseCorrectParams: VideoChannelUpdate = { | ||
173 | displayName: 'hello', | ||
174 | description: 'super description', | ||
175 | support: 'toto', | ||
176 | bulkVideosSupportUpdate: false | ||
177 | } | ||
178 | let path: string | ||
179 | |||
180 | before(async function () { | ||
181 | path = videoChannelPath + '/super_channel' | ||
182 | }) | ||
183 | |||
184 | it('Should fail with a non authenticated user', async function () { | ||
185 | await makePutBodyRequest({ | ||
186 | url: server.url, | ||
187 | path, | ||
188 | token: 'hi', | ||
189 | fields: baseCorrectParams, | ||
190 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
191 | }) | ||
192 | }) | ||
193 | |||
194 | it('Should fail with another authenticated user', async function () { | ||
195 | await makePutBodyRequest({ | ||
196 | url: server.url, | ||
197 | path, | ||
198 | token: userInfo.accessToken, | ||
199 | fields: baseCorrectParams, | ||
200 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
201 | }) | ||
202 | }) | ||
203 | |||
204 | it('Should fail with a long name', async function () { | ||
205 | const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) } | ||
206 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
207 | }) | ||
208 | |||
209 | it('Should fail with a long description', async function () { | ||
210 | const fields = { ...baseCorrectParams, description: 'super'.repeat(201) } | ||
211 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
212 | }) | ||
213 | |||
214 | it('Should fail with a long support text', async function () { | ||
215 | const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } | ||
216 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
217 | }) | ||
218 | |||
219 | it('Should fail with a bad bulkVideosSupportUpdate field', async function () { | ||
220 | const fields = { ...baseCorrectParams, bulkVideosSupportUpdate: 'super' } | ||
221 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
222 | }) | ||
223 | |||
224 | it('Should succeed with the correct parameters', async function () { | ||
225 | await makePutBodyRequest({ | ||
226 | url: server.url, | ||
227 | path, | ||
228 | token: server.accessToken, | ||
229 | fields: baseCorrectParams, | ||
230 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
231 | }) | ||
232 | }) | ||
233 | }) | ||
234 | |||
235 | describe('When updating video channel avatars/banners', function () { | ||
236 | const types = [ 'avatar', 'banner' ] | ||
237 | let path: string | ||
238 | |||
239 | before(async function () { | ||
240 | path = videoChannelPath + '/super_channel' | ||
241 | }) | ||
242 | |||
243 | it('Should fail with an incorrect input file', async function () { | ||
244 | for (const type of types) { | ||
245 | const fields = {} | ||
246 | const attaches = { | ||
247 | [type + 'file']: buildAbsoluteFixturePath('video_short.mp4') | ||
248 | } | ||
249 | |||
250 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) | ||
251 | } | ||
252 | }) | ||
253 | |||
254 | it('Should fail with a big file', async function () { | ||
255 | for (const type of types) { | ||
256 | const fields = {} | ||
257 | const attaches = { | ||
258 | [type + 'file']: buildAbsoluteFixturePath('avatar-big.png') | ||
259 | } | ||
260 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) | ||
261 | } | ||
262 | }) | ||
263 | |||
264 | it('Should fail with an unauthenticated user', async function () { | ||
265 | for (const type of types) { | ||
266 | const fields = {} | ||
267 | const attaches = { | ||
268 | [type + 'file']: buildAbsoluteFixturePath('avatar.png') | ||
269 | } | ||
270 | await makeUploadRequest({ | ||
271 | url: server.url, | ||
272 | path: `${path}/${type}/pick`, | ||
273 | fields, | ||
274 | attaches, | ||
275 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
276 | }) | ||
277 | } | ||
278 | }) | ||
279 | |||
280 | it('Should succeed with the correct params', async function () { | ||
281 | for (const type of types) { | ||
282 | const fields = {} | ||
283 | const attaches = { | ||
284 | [type + 'file']: buildAbsoluteFixturePath('avatar.png') | ||
285 | } | ||
286 | await makeUploadRequest({ | ||
287 | url: server.url, | ||
288 | path: `${path}/${type}/pick`, | ||
289 | token: server.accessToken, | ||
290 | fields, | ||
291 | attaches, | ||
292 | expectedStatus: HttpStatusCode.OK_200 | ||
293 | }) | ||
294 | } | ||
295 | }) | ||
296 | }) | ||
297 | |||
298 | describe('When getting a video channel', function () { | ||
299 | it('Should return the list of the video channels with nothing', async function () { | ||
300 | const res = await makeGetRequest({ | ||
301 | url: server.url, | ||
302 | path: videoChannelPath, | ||
303 | expectedStatus: HttpStatusCode.OK_200 | ||
304 | }) | ||
305 | |||
306 | expect(res.body.data).to.be.an('array') | ||
307 | }) | ||
308 | |||
309 | it('Should return 404 with an incorrect video channel', async function () { | ||
310 | await makeGetRequest({ | ||
311 | url: server.url, | ||
312 | path: videoChannelPath + '/super_channel2', | ||
313 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
314 | }) | ||
315 | }) | ||
316 | |||
317 | it('Should succeed with the correct parameters', async function () { | ||
318 | await makeGetRequest({ | ||
319 | url: server.url, | ||
320 | path: videoChannelPath + '/super_channel', | ||
321 | expectedStatus: HttpStatusCode.OK_200 | ||
322 | }) | ||
323 | }) | ||
324 | }) | ||
325 | |||
326 | describe('When getting channel followers', function () { | ||
327 | const path = '/api/v1/video-channels/super_channel/followers' | ||
328 | |||
329 | it('Should fail with a bad start pagination', async function () { | ||
330 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
331 | }) | ||
332 | |||
333 | it('Should fail with a bad count pagination', async function () { | ||
334 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
335 | }) | ||
336 | |||
337 | it('Should fail with an incorrect sort', async function () { | ||
338 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
339 | }) | ||
340 | |||
341 | it('Should fail with a unauthenticated user', async function () { | ||
342 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
343 | }) | ||
344 | |||
345 | it('Should fail with a another user', async function () { | ||
346 | await makeGetRequest({ url: server.url, path, token: userInfo.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
347 | }) | ||
348 | |||
349 | it('Should succeed with the correct params', async function () { | ||
350 | await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
351 | }) | ||
352 | }) | ||
353 | |||
354 | describe('When deleting a video channel', function () { | ||
355 | it('Should fail with a non authenticated user', async function () { | ||
356 | await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
357 | }) | ||
358 | |||
359 | it('Should fail with another authenticated user', async function () { | ||
360 | await command.delete({ token: userInfo.accessToken, channelName: 'super_channel', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
361 | }) | ||
362 | |||
363 | it('Should fail with an unknown video channel id', async function () { | ||
364 | await command.delete({ channelName: 'super_channel2', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
365 | }) | ||
366 | |||
367 | it('Should succeed with the correct parameters', async function () { | ||
368 | await command.delete({ channelName: 'super_channel' }) | ||
369 | }) | ||
370 | |||
371 | it('Should fail to delete the last user video channel', async function () { | ||
372 | await command.delete({ channelName: 'root_channel', expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
373 | }) | ||
374 | }) | ||
375 | |||
376 | after(async function () { | ||
377 | await cleanupTests([ server ]) | ||
378 | }) | ||
379 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-comments.ts b/packages/tests/src/api/check-params/video-comments.ts new file mode 100644 index 000000000..177361606 --- /dev/null +++ b/packages/tests/src/api/check-params/video-comments.ts | |||
@@ -0,0 +1,484 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
5 | import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | makeDeleteRequest, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Test video comments API validator', function () { | ||
17 | let pathThread: string | ||
18 | let pathComment: string | ||
19 | |||
20 | let server: PeerTubeServer | ||
21 | |||
22 | let video: VideoCreateResult | ||
23 | |||
24 | let userAccessToken: string | ||
25 | let userAccessToken2: string | ||
26 | |||
27 | let commentId: number | ||
28 | let privateCommentId: number | ||
29 | let privateVideo: VideoCreateResult | ||
30 | |||
31 | // --------------------------------------------------------------- | ||
32 | |||
33 | before(async function () { | ||
34 | this.timeout(120000) | ||
35 | |||
36 | server = await createSingleServer(1) | ||
37 | |||
38 | await setAccessTokensToServers([ server ]) | ||
39 | |||
40 | { | ||
41 | video = await server.videos.upload({ attributes: {} }) | ||
42 | pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads' | ||
43 | } | ||
44 | |||
45 | { | ||
46 | privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } }) | ||
47 | } | ||
48 | |||
49 | { | ||
50 | const created = await server.comments.createThread({ videoId: video.uuid, text: 'coucou' }) | ||
51 | commentId = created.id | ||
52 | pathComment = '/api/v1/videos/' + video.uuid + '/comments/' + commentId | ||
53 | } | ||
54 | |||
55 | { | ||
56 | const created = await server.comments.createThread({ videoId: privateVideo.uuid, text: 'coucou' }) | ||
57 | privateCommentId = created.id | ||
58 | } | ||
59 | |||
60 | { | ||
61 | const user = { username: 'user1', password: 'my super password' } | ||
62 | await server.users.create({ username: user.username, password: user.password }) | ||
63 | userAccessToken = await server.login.getAccessToken(user) | ||
64 | } | ||
65 | |||
66 | { | ||
67 | const user = { username: 'user2', password: 'my super password' } | ||
68 | await server.users.create({ username: user.username, password: user.password }) | ||
69 | userAccessToken2 = await server.login.getAccessToken(user) | ||
70 | } | ||
71 | }) | ||
72 | |||
73 | describe('When listing video comment threads', function () { | ||
74 | it('Should fail with a bad start pagination', async function () { | ||
75 | await checkBadStartPagination(server.url, pathThread, server.accessToken) | ||
76 | }) | ||
77 | |||
78 | it('Should fail with a bad count pagination', async function () { | ||
79 | await checkBadCountPagination(server.url, pathThread, server.accessToken) | ||
80 | }) | ||
81 | |||
82 | it('Should fail with an incorrect sort', async function () { | ||
83 | await checkBadSortPagination(server.url, pathThread, server.accessToken) | ||
84 | }) | ||
85 | |||
86 | it('Should fail with an incorrect video', async function () { | ||
87 | await makeGetRequest({ | ||
88 | url: server.url, | ||
89 | path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads', | ||
90 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
91 | }) | ||
92 | }) | ||
93 | |||
94 | it('Should fail with a private video without token', async function () { | ||
95 | await makeGetRequest({ | ||
96 | url: server.url, | ||
97 | path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads', | ||
98 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
99 | }) | ||
100 | }) | ||
101 | |||
102 | it('Should fail with another user token', async function () { | ||
103 | await makeGetRequest({ | ||
104 | url: server.url, | ||
105 | token: userAccessToken, | ||
106 | path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads', | ||
107 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
108 | }) | ||
109 | }) | ||
110 | |||
111 | it('Should succeed with the correct params', async function () { | ||
112 | await makeGetRequest({ | ||
113 | url: server.url, | ||
114 | token: server.accessToken, | ||
115 | path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads', | ||
116 | expectedStatus: HttpStatusCode.OK_200 | ||
117 | }) | ||
118 | }) | ||
119 | }) | ||
120 | |||
121 | describe('When listing comments of a thread', function () { | ||
122 | it('Should fail with an incorrect video', async function () { | ||
123 | await makeGetRequest({ | ||
124 | url: server.url, | ||
125 | path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads/' + commentId, | ||
126 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
127 | }) | ||
128 | }) | ||
129 | |||
130 | it('Should fail with an incorrect thread id', async function () { | ||
131 | await makeGetRequest({ | ||
132 | url: server.url, | ||
133 | path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/156', | ||
134 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
135 | }) | ||
136 | }) | ||
137 | |||
138 | it('Should fail with a private video without token', async function () { | ||
139 | await makeGetRequest({ | ||
140 | url: server.url, | ||
141 | path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId, | ||
142 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
143 | }) | ||
144 | }) | ||
145 | |||
146 | it('Should fail with another user token', async function () { | ||
147 | await makeGetRequest({ | ||
148 | url: server.url, | ||
149 | token: userAccessToken, | ||
150 | path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId, | ||
151 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
152 | }) | ||
153 | }) | ||
154 | |||
155 | it('Should success with the correct params', async function () { | ||
156 | await makeGetRequest({ | ||
157 | url: server.url, | ||
158 | token: server.accessToken, | ||
159 | path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId, | ||
160 | expectedStatus: HttpStatusCode.OK_200 | ||
161 | }) | ||
162 | |||
163 | await makeGetRequest({ | ||
164 | url: server.url, | ||
165 | path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/' + commentId, | ||
166 | expectedStatus: HttpStatusCode.OK_200 | ||
167 | }) | ||
168 | }) | ||
169 | }) | ||
170 | |||
171 | describe('When adding a video thread', function () { | ||
172 | |||
173 | it('Should fail with a non authenticated user', async function () { | ||
174 | const fields = { | ||
175 | text: 'text' | ||
176 | } | ||
177 | await makePostBodyRequest({ | ||
178 | url: server.url, | ||
179 | path: pathThread, | ||
180 | token: 'none', | ||
181 | fields, | ||
182 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
183 | }) | ||
184 | }) | ||
185 | |||
186 | it('Should fail with nothing', async function () { | ||
187 | const fields = {} | ||
188 | await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields }) | ||
189 | }) | ||
190 | |||
191 | it('Should fail with a short comment', async function () { | ||
192 | const fields = { | ||
193 | text: '' | ||
194 | } | ||
195 | await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields }) | ||
196 | }) | ||
197 | |||
198 | it('Should fail with a long comment', async function () { | ||
199 | const fields = { | ||
200 | text: 'h'.repeat(10001) | ||
201 | } | ||
202 | await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields }) | ||
203 | }) | ||
204 | |||
205 | it('Should fail with an incorrect video', async function () { | ||
206 | const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads' | ||
207 | const fields = { text: 'super comment' } | ||
208 | |||
209 | await makePostBodyRequest({ | ||
210 | url: server.url, | ||
211 | path, | ||
212 | token: server.accessToken, | ||
213 | fields, | ||
214 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
215 | }) | ||
216 | }) | ||
217 | |||
218 | it('Should fail with a private video of another user', async function () { | ||
219 | const fields = { text: 'super comment' } | ||
220 | |||
221 | await makePostBodyRequest({ | ||
222 | url: server.url, | ||
223 | path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads', | ||
224 | token: userAccessToken, | ||
225 | fields, | ||
226 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
227 | }) | ||
228 | }) | ||
229 | |||
230 | it('Should succeed with the correct parameters', async function () { | ||
231 | const fields = { text: 'super comment' } | ||
232 | |||
233 | await makePostBodyRequest({ | ||
234 | url: server.url, | ||
235 | path: pathThread, | ||
236 | token: server.accessToken, | ||
237 | fields, | ||
238 | expectedStatus: HttpStatusCode.OK_200 | ||
239 | }) | ||
240 | }) | ||
241 | }) | ||
242 | |||
243 | describe('When adding a comment to a thread', function () { | ||
244 | |||
245 | it('Should fail with a non authenticated user', async function () { | ||
246 | const fields = { | ||
247 | text: 'text' | ||
248 | } | ||
249 | await makePostBodyRequest({ | ||
250 | url: server.url, | ||
251 | path: pathComment, | ||
252 | token: 'none', | ||
253 | fields, | ||
254 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
255 | }) | ||
256 | }) | ||
257 | |||
258 | it('Should fail with nothing', async function () { | ||
259 | const fields = {} | ||
260 | await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields }) | ||
261 | }) | ||
262 | |||
263 | it('Should fail with a short comment', async function () { | ||
264 | const fields = { | ||
265 | text: '' | ||
266 | } | ||
267 | await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields }) | ||
268 | }) | ||
269 | |||
270 | it('Should fail with a long comment', async function () { | ||
271 | const fields = { | ||
272 | text: 'h'.repeat(10001) | ||
273 | } | ||
274 | await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields }) | ||
275 | }) | ||
276 | |||
277 | it('Should fail with an incorrect video', async function () { | ||
278 | const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId | ||
279 | const fields = { | ||
280 | text: 'super comment' | ||
281 | } | ||
282 | await makePostBodyRequest({ | ||
283 | url: server.url, | ||
284 | path, | ||
285 | token: server.accessToken, | ||
286 | fields, | ||
287 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
288 | }) | ||
289 | }) | ||
290 | |||
291 | it('Should fail with a private video of another user', async function () { | ||
292 | const fields = { text: 'super comment' } | ||
293 | |||
294 | await makePostBodyRequest({ | ||
295 | url: server.url, | ||
296 | path: '/api/v1/videos/' + privateVideo.uuid + '/comments/' + privateCommentId, | ||
297 | token: userAccessToken, | ||
298 | fields, | ||
299 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
300 | }) | ||
301 | }) | ||
302 | |||
303 | it('Should fail with an incorrect comment', async function () { | ||
304 | const path = '/api/v1/videos/' + video.uuid + '/comments/124' | ||
305 | const fields = { | ||
306 | text: 'super comment' | ||
307 | } | ||
308 | await makePostBodyRequest({ | ||
309 | url: server.url, | ||
310 | path, | ||
311 | token: server.accessToken, | ||
312 | fields, | ||
313 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
314 | }) | ||
315 | }) | ||
316 | |||
317 | it('Should succeed with the correct parameters', async function () { | ||
318 | const fields = { | ||
319 | text: 'super comment' | ||
320 | } | ||
321 | await makePostBodyRequest({ | ||
322 | url: server.url, | ||
323 | path: pathComment, | ||
324 | token: server.accessToken, | ||
325 | fields, | ||
326 | expectedStatus: HttpStatusCode.OK_200 | ||
327 | }) | ||
328 | }) | ||
329 | }) | ||
330 | |||
331 | describe('When removing video comments', function () { | ||
332 | it('Should fail with a non authenticated user', async function () { | ||
333 | await makeDeleteRequest({ url: server.url, path: pathComment, token: 'none', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
334 | }) | ||
335 | |||
336 | it('Should fail with another user', async function () { | ||
337 | await makeDeleteRequest({ | ||
338 | url: server.url, | ||
339 | path: pathComment, | ||
340 | token: userAccessToken, | ||
341 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
342 | }) | ||
343 | }) | ||
344 | |||
345 | it('Should fail with an incorrect video', async function () { | ||
346 | const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId | ||
347 | await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
348 | }) | ||
349 | |||
350 | it('Should fail with an incorrect comment', async function () { | ||
351 | const path = '/api/v1/videos/' + video.uuid + '/comments/124' | ||
352 | await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
353 | }) | ||
354 | |||
355 | it('Should succeed with the same user', async function () { | ||
356 | let commentToDelete: number | ||
357 | |||
358 | { | ||
359 | const created = await server.comments.createThread({ videoId: video.uuid, token: userAccessToken, text: 'hello' }) | ||
360 | commentToDelete = created.id | ||
361 | } | ||
362 | |||
363 | const path = '/api/v1/videos/' + video.uuid + '/comments/' + commentToDelete | ||
364 | |||
365 | await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
366 | await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
367 | }) | ||
368 | |||
369 | it('Should succeed with the owner of the video', async function () { | ||
370 | let commentToDelete: number | ||
371 | let anotherVideoUUID: string | ||
372 | |||
373 | { | ||
374 | const { uuid } = await server.videos.upload({ token: userAccessToken, attributes: { name: 'video' } }) | ||
375 | anotherVideoUUID = uuid | ||
376 | } | ||
377 | |||
378 | { | ||
379 | const created = await server.comments.createThread({ videoId: anotherVideoUUID, text: 'hello' }) | ||
380 | commentToDelete = created.id | ||
381 | } | ||
382 | |||
383 | const path = '/api/v1/videos/' + anotherVideoUUID + '/comments/' + commentToDelete | ||
384 | |||
385 | await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
386 | await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
387 | }) | ||
388 | |||
389 | it('Should succeed with the correct parameters', async function () { | ||
390 | await makeDeleteRequest({ | ||
391 | url: server.url, | ||
392 | path: pathComment, | ||
393 | token: server.accessToken, | ||
394 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
395 | }) | ||
396 | }) | ||
397 | }) | ||
398 | |||
399 | describe('When a video has comments disabled', function () { | ||
400 | before(async function () { | ||
401 | video = await server.videos.upload({ attributes: { commentsEnabled: false } }) | ||
402 | pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads' | ||
403 | }) | ||
404 | |||
405 | it('Should return an empty thread list', async function () { | ||
406 | const res = await makeGetRequest({ | ||
407 | url: server.url, | ||
408 | path: pathThread, | ||
409 | expectedStatus: HttpStatusCode.OK_200 | ||
410 | }) | ||
411 | expect(res.body.total).to.equal(0) | ||
412 | expect(res.body.data).to.have.lengthOf(0) | ||
413 | }) | ||
414 | |||
415 | it('Should return an thread comments list') | ||
416 | |||
417 | it('Should return conflict on thread add', async function () { | ||
418 | const fields = { | ||
419 | text: 'super comment' | ||
420 | } | ||
421 | await makePostBodyRequest({ | ||
422 | url: server.url, | ||
423 | path: pathThread, | ||
424 | token: server.accessToken, | ||
425 | fields, | ||
426 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
427 | }) | ||
428 | }) | ||
429 | |||
430 | it('Should return conflict on comment thread add') | ||
431 | }) | ||
432 | |||
433 | describe('When listing admin comments threads', function () { | ||
434 | const path = '/api/v1/videos/comments' | ||
435 | |||
436 | it('Should fail with a bad start pagination', async function () { | ||
437 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
438 | }) | ||
439 | |||
440 | it('Should fail with a bad count pagination', async function () { | ||
441 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
442 | }) | ||
443 | |||
444 | it('Should fail with an incorrect sort', async function () { | ||
445 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
446 | }) | ||
447 | |||
448 | it('Should fail with a non authenticated user', async function () { | ||
449 | await makeGetRequest({ | ||
450 | url: server.url, | ||
451 | path, | ||
452 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
453 | }) | ||
454 | }) | ||
455 | |||
456 | it('Should fail with a non admin user', async function () { | ||
457 | await makeGetRequest({ | ||
458 | url: server.url, | ||
459 | path, | ||
460 | token: userAccessToken, | ||
461 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
462 | }) | ||
463 | }) | ||
464 | |||
465 | it('Should succeed with the correct params', async function () { | ||
466 | await makeGetRequest({ | ||
467 | url: server.url, | ||
468 | path, | ||
469 | token: server.accessToken, | ||
470 | query: { | ||
471 | isLocal: false, | ||
472 | search: 'toto', | ||
473 | searchAccount: 'toto', | ||
474 | searchVideo: 'toto' | ||
475 | }, | ||
476 | expectedStatus: HttpStatusCode.OK_200 | ||
477 | }) | ||
478 | }) | ||
479 | }) | ||
480 | |||
481 | after(async function () { | ||
482 | await cleanupTests([ server ]) | ||
483 | }) | ||
484 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-files.ts b/packages/tests/src/api/check-params/video-files.ts new file mode 100644 index 000000000..b5819ff19 --- /dev/null +++ b/packages/tests/src/api/check-params/video-files.ts | |||
@@ -0,0 +1,195 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { getAllFiles } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode, UserRole, VideoDetails, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | makeRawRequest, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test videos files', function () { | ||
16 | let servers: PeerTubeServer[] | ||
17 | |||
18 | let userToken: string | ||
19 | let moderatorToken: string | ||
20 | |||
21 | // --------------------------------------------------------------- | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(300_000) | ||
25 | |||
26 | servers = await createMultipleServers(2) | ||
27 | await setAccessTokensToServers(servers) | ||
28 | |||
29 | await doubleFollow(servers[0], servers[1]) | ||
30 | |||
31 | userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) | ||
32 | moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) | ||
33 | }) | ||
34 | |||
35 | describe('Getting metadata', function () { | ||
36 | let video: VideoDetails | ||
37 | |||
38 | before(async function () { | ||
39 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
40 | video = await servers[0].videos.getWithToken({ id: uuid }) | ||
41 | }) | ||
42 | |||
43 | it('Should not get metadata of private video without token', async function () { | ||
44 | for (const file of getAllFiles(video)) { | ||
45 | await makeRawRequest({ url: file.metadataUrl, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
46 | } | ||
47 | }) | ||
48 | |||
49 | it('Should not get metadata of private video without the appropriate token', async function () { | ||
50 | for (const file of getAllFiles(video)) { | ||
51 | await makeRawRequest({ url: file.metadataUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
52 | } | ||
53 | }) | ||
54 | |||
55 | it('Should get metadata of private video with the appropriate token', async function () { | ||
56 | for (const file of getAllFiles(video)) { | ||
57 | await makeRawRequest({ url: file.metadataUrl, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
58 | } | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | describe('Deleting files', function () { | ||
63 | let webVideoId: string | ||
64 | let hlsId: string | ||
65 | let remoteId: string | ||
66 | |||
67 | let validId1: string | ||
68 | let validId2: string | ||
69 | |||
70 | let hlsFileId: number | ||
71 | let webVideoFileId: number | ||
72 | |||
73 | let remoteHLSFileId: number | ||
74 | let remoteWebVideoFileId: number | ||
75 | |||
76 | before(async function () { | ||
77 | this.timeout(300_000) | ||
78 | |||
79 | { | ||
80 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) | ||
81 | await waitJobs(servers) | ||
82 | |||
83 | const video = await servers[1].videos.get({ id: uuid }) | ||
84 | remoteId = video.uuid | ||
85 | remoteHLSFileId = video.streamingPlaylists[0].files[0].id | ||
86 | remoteWebVideoFileId = video.files[0].id | ||
87 | } | ||
88 | |||
89 | { | ||
90 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) | ||
91 | |||
92 | { | ||
93 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) | ||
94 | await waitJobs(servers) | ||
95 | |||
96 | const video = await servers[0].videos.get({ id: uuid }) | ||
97 | validId1 = video.uuid | ||
98 | hlsFileId = video.streamingPlaylists[0].files[0].id | ||
99 | webVideoFileId = video.files[0].id | ||
100 | } | ||
101 | |||
102 | { | ||
103 | const { uuid } = await servers[0].videos.quickUpload({ name: 'both 2' }) | ||
104 | validId2 = uuid | ||
105 | } | ||
106 | } | ||
107 | |||
108 | await waitJobs(servers) | ||
109 | |||
110 | { | ||
111 | await servers[0].config.enableTranscoding({ hls: true, webVideo: false }) | ||
112 | const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' }) | ||
113 | hlsId = uuid | ||
114 | } | ||
115 | |||
116 | await waitJobs(servers) | ||
117 | |||
118 | { | ||
119 | await servers[0].config.enableTranscoding({ webVideo: true, hls: false }) | ||
120 | const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' }) | ||
121 | webVideoId = uuid | ||
122 | } | ||
123 | |||
124 | await waitJobs(servers) | ||
125 | }) | ||
126 | |||
127 | it('Should not delete files of a unknown video', async function () { | ||
128 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | ||
129 | |||
130 | await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) | ||
131 | await servers[0].videos.removeAllWebVideoFiles({ videoId: 404, expectedStatus }) | ||
132 | |||
133 | await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) | ||
134 | await servers[0].videos.removeWebVideoFile({ videoId: 404, fileId: webVideoFileId, expectedStatus }) | ||
135 | }) | ||
136 | |||
137 | it('Should not delete unknown files', async function () { | ||
138 | const expectedStatus = HttpStatusCode.NOT_FOUND_404 | ||
139 | |||
140 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webVideoFileId, expectedStatus }) | ||
141 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) | ||
142 | }) | ||
143 | |||
144 | it('Should not delete files of a remote video', async function () { | ||
145 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
146 | |||
147 | await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) | ||
148 | await servers[0].videos.removeAllWebVideoFiles({ videoId: remoteId, expectedStatus }) | ||
149 | |||
150 | await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) | ||
151 | await servers[0].videos.removeWebVideoFile({ videoId: remoteId, fileId: remoteWebVideoFileId, expectedStatus }) | ||
152 | }) | ||
153 | |||
154 | it('Should not delete files by a non admin user', async function () { | ||
155 | const expectedStatus = HttpStatusCode.FORBIDDEN_403 | ||
156 | |||
157 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) | ||
158 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) | ||
159 | |||
160 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: userToken, expectedStatus }) | ||
161 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) | ||
162 | |||
163 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus }) | ||
164 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus }) | ||
165 | |||
166 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: userToken, expectedStatus }) | ||
167 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: moderatorToken, expectedStatus }) | ||
168 | }) | ||
169 | |||
170 | it('Should not delete files if the files are not available', async function () { | ||
171 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
172 | await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
173 | |||
174 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
175 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
176 | }) | ||
177 | |||
178 | it('Should not delete files if no both versions are available', async function () { | ||
179 | await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
180 | await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
181 | }) | ||
182 | |||
183 | it('Should delete files if both versions are available', async function () { | ||
184 | await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) | ||
185 | await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId }) | ||
186 | |||
187 | await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) | ||
188 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId2 }) | ||
189 | }) | ||
190 | }) | ||
191 | |||
192 | after(async function () { | ||
193 | await cleanupTests(servers) | ||
194 | }) | ||
195 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-imports.ts b/packages/tests/src/api/check-params/video-imports.ts new file mode 100644 index 000000000..e078cedd6 --- /dev/null +++ b/packages/tests/src/api/check-params/video-imports.ts | |||
@@ -0,0 +1,433 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { omit } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
6 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | makeGetRequest, | ||
12 | makePostBodyRequest, | ||
13 | makeUploadRequest, | ||
14 | PeerTubeServer, | ||
15 | setAccessTokensToServers, | ||
16 | setDefaultVideoChannel, | ||
17 | waitJobs | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | |||
20 | describe('Test video imports API validator', function () { | ||
21 | const path = '/api/v1/videos/imports' | ||
22 | let server: PeerTubeServer | ||
23 | let userAccessToken = '' | ||
24 | let channelId: number | ||
25 | |||
26 | // --------------------------------------------------------------- | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(30000) | ||
30 | |||
31 | server = await createSingleServer(1) | ||
32 | |||
33 | await setAccessTokensToServers([ server ]) | ||
34 | await setDefaultVideoChannel([ server ]) | ||
35 | |||
36 | const username = 'user1' | ||
37 | const password = 'my super password' | ||
38 | await server.users.create({ username, password }) | ||
39 | userAccessToken = await server.login.getAccessToken({ username, password }) | ||
40 | |||
41 | { | ||
42 | const { videoChannels } = await server.users.getMyInfo() | ||
43 | channelId = videoChannels[0].id | ||
44 | } | ||
45 | }) | ||
46 | |||
47 | describe('When listing my video imports', function () { | ||
48 | const myPath = '/api/v1/users/me/videos/imports' | ||
49 | |||
50 | it('Should fail with a bad start pagination', async function () { | ||
51 | await checkBadStartPagination(server.url, myPath, server.accessToken) | ||
52 | }) | ||
53 | |||
54 | it('Should fail with a bad count pagination', async function () { | ||
55 | await checkBadCountPagination(server.url, myPath, server.accessToken) | ||
56 | }) | ||
57 | |||
58 | it('Should fail with an incorrect sort', async function () { | ||
59 | await checkBadSortPagination(server.url, myPath, server.accessToken) | ||
60 | }) | ||
61 | |||
62 | it('Should fail with a bad videoChannelSyncId param', async function () { | ||
63 | await makeGetRequest({ | ||
64 | url: server.url, | ||
65 | path: myPath, | ||
66 | query: { videoChannelSyncId: 'toto' }, | ||
67 | token: server.accessToken | ||
68 | }) | ||
69 | }) | ||
70 | |||
71 | it('Should success with the correct parameters', async function () { | ||
72 | await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken }) | ||
73 | }) | ||
74 | }) | ||
75 | |||
76 | describe('When adding a video import', function () { | ||
77 | let baseCorrectParams | ||
78 | |||
79 | before(function () { | ||
80 | baseCorrectParams = { | ||
81 | targetUrl: FIXTURE_URLS.goodVideo, | ||
82 | name: 'my super name', | ||
83 | category: 5, | ||
84 | licence: 1, | ||
85 | language: 'pt', | ||
86 | nsfw: false, | ||
87 | commentsEnabled: true, | ||
88 | downloadEnabled: true, | ||
89 | waitTranscoding: true, | ||
90 | description: 'my super description', | ||
91 | support: 'my super support text', | ||
92 | tags: [ 'tag1', 'tag2' ], | ||
93 | privacy: VideoPrivacy.PUBLIC, | ||
94 | channelId | ||
95 | } | ||
96 | }) | ||
97 | |||
98 | it('Should fail with nothing', async function () { | ||
99 | const fields = {} | ||
100 | await makePostBodyRequest({ | ||
101 | url: server.url, | ||
102 | path, | ||
103 | token: server.accessToken, | ||
104 | fields, | ||
105 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
106 | }) | ||
107 | }) | ||
108 | |||
109 | it('Should fail without a target url', async function () { | ||
110 | const fields = omit(baseCorrectParams, [ 'targetUrl' ]) | ||
111 | await makePostBodyRequest({ | ||
112 | url: server.url, | ||
113 | path, | ||
114 | token: server.accessToken, | ||
115 | fields, | ||
116 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
117 | }) | ||
118 | }) | ||
119 | |||
120 | it('Should fail with a bad target url', async function () { | ||
121 | const fields = { ...baseCorrectParams, targetUrl: 'htt://hello' } | ||
122 | |||
123 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
124 | }) | ||
125 | |||
126 | it('Should fail with localhost', async function () { | ||
127 | const fields = { ...baseCorrectParams, targetUrl: 'http://localhost:8000' } | ||
128 | |||
129 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
130 | }) | ||
131 | |||
132 | it('Should fail with a private IP target urls', async function () { | ||
133 | const targetUrls = [ | ||
134 | 'http://127.0.0.1:8000', | ||
135 | 'http://127.0.0.1', | ||
136 | 'http://127.0.0.1/hello', | ||
137 | 'https://192.168.1.42', | ||
138 | 'http://192.168.1.42', | ||
139 | 'http://127.0.0.1.cpy.re' | ||
140 | ] | ||
141 | |||
142 | for (const targetUrl of targetUrls) { | ||
143 | const fields = { ...baseCorrectParams, targetUrl } | ||
144 | |||
145 | await makePostBodyRequest({ | ||
146 | url: server.url, | ||
147 | path, | ||
148 | token: server.accessToken, | ||
149 | fields, | ||
150 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
151 | }) | ||
152 | } | ||
153 | }) | ||
154 | |||
155 | it('Should fail with a long name', async function () { | ||
156 | const fields = { ...baseCorrectParams, name: 'super'.repeat(65) } | ||
157 | |||
158 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
159 | }) | ||
160 | |||
161 | it('Should fail with a bad category', async function () { | ||
162 | const fields = { ...baseCorrectParams, category: 125 } | ||
163 | |||
164 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
165 | }) | ||
166 | |||
167 | it('Should fail with a bad licence', async function () { | ||
168 | const fields = { ...baseCorrectParams, licence: 125 } | ||
169 | |||
170 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
171 | }) | ||
172 | |||
173 | it('Should fail with a bad language', async function () { | ||
174 | const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } | ||
175 | |||
176 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
177 | }) | ||
178 | |||
179 | it('Should fail with a long description', async function () { | ||
180 | const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) } | ||
181 | |||
182 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
183 | }) | ||
184 | |||
185 | it('Should fail with a long support text', async function () { | ||
186 | const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } | ||
187 | |||
188 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
189 | }) | ||
190 | |||
191 | it('Should fail without a channel', async function () { | ||
192 | const fields = omit(baseCorrectParams, [ 'channelId' ]) | ||
193 | |||
194 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
195 | }) | ||
196 | |||
197 | it('Should fail with a bad channel', async function () { | ||
198 | const fields = { ...baseCorrectParams, channelId: 545454 } | ||
199 | |||
200 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
201 | }) | ||
202 | |||
203 | it('Should fail with another user channel', async function () { | ||
204 | const user = { | ||
205 | username: 'fake', | ||
206 | password: 'fake_password' | ||
207 | } | ||
208 | await server.users.create({ username: user.username, password: user.password }) | ||
209 | |||
210 | const accessTokenUser = await server.login.getAccessToken(user) | ||
211 | const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser }) | ||
212 | const customChannelId = videoChannels[0].id | ||
213 | |||
214 | const fields = { ...baseCorrectParams, channelId: customChannelId } | ||
215 | |||
216 | await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields }) | ||
217 | }) | ||
218 | |||
219 | it('Should fail with too many tags', async function () { | ||
220 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] } | ||
221 | |||
222 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
223 | }) | ||
224 | |||
225 | it('Should fail with a tag length too low', async function () { | ||
226 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] } | ||
227 | |||
228 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
229 | }) | ||
230 | |||
231 | it('Should fail with a tag length too big', async function () { | ||
232 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] } | ||
233 | |||
234 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
235 | }) | ||
236 | |||
237 | it('Should fail with an incorrect thumbnail file', async function () { | ||
238 | const fields = baseCorrectParams | ||
239 | const attaches = { | ||
240 | thumbnailfile: buildAbsoluteFixturePath('video_short.mp4') | ||
241 | } | ||
242 | |||
243 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
244 | }) | ||
245 | |||
246 | it('Should fail with a big thumbnail file', async function () { | ||
247 | const fields = baseCorrectParams | ||
248 | const attaches = { | ||
249 | thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') | ||
250 | } | ||
251 | |||
252 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
253 | }) | ||
254 | |||
255 | it('Should fail with an incorrect preview file', async function () { | ||
256 | const fields = baseCorrectParams | ||
257 | const attaches = { | ||
258 | previewfile: buildAbsoluteFixturePath('video_short.mp4') | ||
259 | } | ||
260 | |||
261 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
262 | }) | ||
263 | |||
264 | it('Should fail with a big preview file', async function () { | ||
265 | const fields = baseCorrectParams | ||
266 | const attaches = { | ||
267 | previewfile: buildAbsoluteFixturePath('custom-preview-big.png') | ||
268 | } | ||
269 | |||
270 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
271 | }) | ||
272 | |||
273 | it('Should fail with an invalid torrent file', async function () { | ||
274 | const fields = omit(baseCorrectParams, [ 'targetUrl' ]) | ||
275 | const attaches = { | ||
276 | torrentfile: buildAbsoluteFixturePath('avatar-big.png') | ||
277 | } | ||
278 | |||
279 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | ||
280 | }) | ||
281 | |||
282 | it('Should fail with an invalid magnet URI', async function () { | ||
283 | let fields = omit(baseCorrectParams, [ 'targetUrl' ]) | ||
284 | fields = { ...fields, magnetUri: 'blabla' } | ||
285 | |||
286 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
287 | }) | ||
288 | |||
289 | it('Should succeed with the correct parameters', async function () { | ||
290 | this.timeout(120000) | ||
291 | |||
292 | await makePostBodyRequest({ | ||
293 | url: server.url, | ||
294 | path, | ||
295 | token: server.accessToken, | ||
296 | fields: baseCorrectParams, | ||
297 | expectedStatus: HttpStatusCode.OK_200 | ||
298 | }) | ||
299 | }) | ||
300 | |||
301 | it('Should forbid to import http videos', async function () { | ||
302 | await server.config.updateCustomSubConfig({ | ||
303 | newConfig: { | ||
304 | import: { | ||
305 | videos: { | ||
306 | http: { | ||
307 | enabled: false | ||
308 | }, | ||
309 | torrent: { | ||
310 | enabled: true | ||
311 | } | ||
312 | } | ||
313 | } | ||
314 | } | ||
315 | }) | ||
316 | |||
317 | await makePostBodyRequest({ | ||
318 | url: server.url, | ||
319 | path, | ||
320 | token: server.accessToken, | ||
321 | fields: baseCorrectParams, | ||
322 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
323 | }) | ||
324 | }) | ||
325 | |||
326 | it('Should forbid to import torrent videos', async function () { | ||
327 | await server.config.updateCustomSubConfig({ | ||
328 | newConfig: { | ||
329 | import: { | ||
330 | videos: { | ||
331 | http: { | ||
332 | enabled: true | ||
333 | }, | ||
334 | torrent: { | ||
335 | enabled: false | ||
336 | } | ||
337 | } | ||
338 | } | ||
339 | } | ||
340 | }) | ||
341 | |||
342 | let fields = omit(baseCorrectParams, [ 'targetUrl' ]) | ||
343 | fields = { ...fields, magnetUri: FIXTURE_URLS.magnet } | ||
344 | |||
345 | await makePostBodyRequest({ | ||
346 | url: server.url, | ||
347 | path, | ||
348 | token: server.accessToken, | ||
349 | fields, | ||
350 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
351 | }) | ||
352 | |||
353 | fields = omit(fields, [ 'magnetUri' ]) | ||
354 | const attaches = { | ||
355 | torrentfile: buildAbsoluteFixturePath('video-720p.torrent') | ||
356 | } | ||
357 | |||
358 | await makeUploadRequest({ | ||
359 | url: server.url, | ||
360 | path, | ||
361 | token: server.accessToken, | ||
362 | fields, | ||
363 | attaches, | ||
364 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
365 | }) | ||
366 | }) | ||
367 | }) | ||
368 | |||
369 | describe('Deleting/cancelling a video import', function () { | ||
370 | let importId: number | ||
371 | |||
372 | async function importVideo () { | ||
373 | const attributes = { channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo } | ||
374 | const res = await server.imports.importVideo({ attributes }) | ||
375 | |||
376 | return res.id | ||
377 | } | ||
378 | |||
379 | before(async function () { | ||
380 | importId = await importVideo() | ||
381 | }) | ||
382 | |||
383 | it('Should fail with an invalid import id', async function () { | ||
384 | await server.imports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
385 | await server.imports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
386 | }) | ||
387 | |||
388 | it('Should fail with an unknown import id', async function () { | ||
389 | await server.imports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
390 | await server.imports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
391 | }) | ||
392 | |||
393 | it('Should fail without token', async function () { | ||
394 | await server.imports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
395 | await server.imports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
396 | }) | ||
397 | |||
398 | it('Should fail with another user token', async function () { | ||
399 | await server.imports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
400 | await server.imports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
401 | }) | ||
402 | |||
403 | it('Should fail to cancel non pending import', async function () { | ||
404 | this.timeout(60000) | ||
405 | |||
406 | await waitJobs([ server ]) | ||
407 | |||
408 | await server.imports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
409 | }) | ||
410 | |||
411 | it('Should succeed to delete an import', async function () { | ||
412 | await server.imports.delete({ importId }) | ||
413 | }) | ||
414 | |||
415 | it('Should fail to delete a pending import', async function () { | ||
416 | await server.jobs.pauseJobQueue() | ||
417 | |||
418 | importId = await importVideo() | ||
419 | |||
420 | await server.imports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
421 | }) | ||
422 | |||
423 | it('Should succeed to cancel an import', async function () { | ||
424 | importId = await importVideo() | ||
425 | |||
426 | await server.imports.cancel({ importId }) | ||
427 | }) | ||
428 | }) | ||
429 | |||
430 | after(async function () { | ||
431 | await cleanupTests([ server ]) | ||
432 | }) | ||
433 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-passwords.ts b/packages/tests/src/api/check-params/video-passwords.ts new file mode 100644 index 000000000..3f57ebe74 --- /dev/null +++ b/packages/tests/src/api/check-params/video-passwords.ts | |||
@@ -0,0 +1,604 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { | ||
3 | HttpStatusCode, | ||
4 | HttpStatusCodeType, | ||
5 | PeerTubeProblemDocument, | ||
6 | ServerErrorCode, | ||
7 | VideoCreateResult, | ||
8 | VideoPrivacy | ||
9 | } from '@peertube/peertube-models' | ||
10 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
11 | import { | ||
12 | cleanupTests, | ||
13 | createSingleServer, | ||
14 | makePostBodyRequest, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
19 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
20 | import { checkUploadVideoParam } from '@tests/shared/videos.js' | ||
21 | |||
22 | describe('Test video passwords validator', function () { | ||
23 | let path: string | ||
24 | let server: PeerTubeServer | ||
25 | let userAccessToken = '' | ||
26 | let video: VideoCreateResult | ||
27 | let channelId: number | ||
28 | let publicVideo: VideoCreateResult | ||
29 | let commentId: number | ||
30 | // --------------------------------------------------------------- | ||
31 | |||
32 | before(async function () { | ||
33 | this.timeout(50000) | ||
34 | |||
35 | server = await createSingleServer(1) | ||
36 | |||
37 | await setAccessTokensToServers([ server ]) | ||
38 | |||
39 | await server.config.updateCustomSubConfig({ | ||
40 | newConfig: { | ||
41 | live: { | ||
42 | enabled: true, | ||
43 | latencySetting: { | ||
44 | enabled: false | ||
45 | }, | ||
46 | allowReplay: false | ||
47 | }, | ||
48 | import: { | ||
49 | videos: { | ||
50 | http:{ | ||
51 | enabled: true | ||
52 | } | ||
53 | } | ||
54 | } | ||
55 | } | ||
56 | }) | ||
57 | |||
58 | userAccessToken = await server.users.generateUserAndToken('user1') | ||
59 | |||
60 | { | ||
61 | const body = await server.users.getMyInfo() | ||
62 | channelId = body.videoChannels[0].id | ||
63 | } | ||
64 | |||
65 | { | ||
66 | video = await server.videos.quickUpload({ | ||
67 | name: 'password protected video', | ||
68 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
69 | videoPasswords: [ 'password1', 'password2' ] | ||
70 | }) | ||
71 | } | ||
72 | path = '/api/v1/videos/' | ||
73 | }) | ||
74 | |||
75 | async function checkVideoPasswordOptions (options: { | ||
76 | server: PeerTubeServer | ||
77 | token: string | ||
78 | videoPasswords: string[] | ||
79 | expectedStatus: HttpStatusCodeType | ||
80 | mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live' | ||
81 | }) { | ||
82 | const { server, token, videoPasswords, expectedStatus = HttpStatusCode.OK_200, mode } = options | ||
83 | const attaches = { | ||
84 | fixture: buildAbsoluteFixturePath('video_short.webm') | ||
85 | } | ||
86 | const baseCorrectParams = { | ||
87 | name: 'my super name', | ||
88 | category: 5, | ||
89 | licence: 1, | ||
90 | language: 'pt', | ||
91 | nsfw: false, | ||
92 | commentsEnabled: true, | ||
93 | downloadEnabled: true, | ||
94 | waitTranscoding: true, | ||
95 | description: 'my super description', | ||
96 | support: 'my super support text', | ||
97 | tags: [ 'tag1', 'tag2' ], | ||
98 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
99 | channelId, | ||
100 | originallyPublishedAt: new Date().toISOString() | ||
101 | } | ||
102 | if (mode === 'uploadLegacy') { | ||
103 | const fields = { ...baseCorrectParams, videoPasswords } | ||
104 | return checkUploadVideoParam({ server, token, attributes: { ...fields, ...attaches }, expectedStatus, mode: 'legacy' }) | ||
105 | } | ||
106 | |||
107 | if (mode === 'uploadResumable') { | ||
108 | const fields = { ...baseCorrectParams, videoPasswords } | ||
109 | return checkUploadVideoParam({ server, token, attributes: { ...fields, ...attaches }, expectedStatus, mode: 'resumable' }) | ||
110 | } | ||
111 | |||
112 | if (mode === 'import') { | ||
113 | const attributes = { ...baseCorrectParams, targetUrl: FIXTURE_URLS.goodVideo, videoPasswords } | ||
114 | return server.imports.importVideo({ attributes, expectedStatus }) | ||
115 | } | ||
116 | |||
117 | if (mode === 'updateVideo') { | ||
118 | const attributes = { ...baseCorrectParams, videoPasswords } | ||
119 | return server.videos.update({ token, expectedStatus, id: video.id, attributes }) | ||
120 | } | ||
121 | |||
122 | if (mode === 'updatePasswords') { | ||
123 | return server.videoPasswords.updateAll({ token, expectedStatus, videoId: video.id, passwords: videoPasswords }) | ||
124 | } | ||
125 | |||
126 | if (mode === 'live') { | ||
127 | const fields = { ...baseCorrectParams, videoPasswords } | ||
128 | |||
129 | return server.live.create({ fields, expectedStatus }) | ||
130 | } | ||
131 | } | ||
132 | |||
133 | function validateVideoPasswordList (mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live') { | ||
134 | |||
135 | it('Should fail with a password protected privacy without providing a password', async function () { | ||
136 | await checkVideoPasswordOptions({ | ||
137 | server, | ||
138 | token: server.accessToken, | ||
139 | videoPasswords: undefined, | ||
140 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
141 | mode | ||
142 | }) | ||
143 | }) | ||
144 | |||
145 | it('Should fail with a password protected privacy and an empty password list', async function () { | ||
146 | const videoPasswords = [] | ||
147 | |||
148 | await checkVideoPasswordOptions({ | ||
149 | server, | ||
150 | token: server.accessToken, | ||
151 | videoPasswords, | ||
152 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
153 | mode | ||
154 | }) | ||
155 | }) | ||
156 | |||
157 | it('Should fail with a password protected privacy and a too short password', async function () { | ||
158 | const videoPasswords = [ 'p' ] | ||
159 | |||
160 | await checkVideoPasswordOptions({ | ||
161 | server, | ||
162 | token: server.accessToken, | ||
163 | videoPasswords, | ||
164 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
165 | mode | ||
166 | }) | ||
167 | }) | ||
168 | |||
169 | it('Should fail with a password protected privacy and a too long password', async function () { | ||
170 | const videoPasswords = [ 'Very very very very very very very very very very very very very very very very very very long password' ] | ||
171 | |||
172 | await checkVideoPasswordOptions({ | ||
173 | server, | ||
174 | token: server.accessToken, | ||
175 | videoPasswords, | ||
176 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
177 | mode | ||
178 | }) | ||
179 | }) | ||
180 | |||
181 | it('Should fail with a password protected privacy and an empty password', async function () { | ||
182 | const videoPasswords = [ '' ] | ||
183 | |||
184 | await checkVideoPasswordOptions({ | ||
185 | server, | ||
186 | token: server.accessToken, | ||
187 | videoPasswords, | ||
188 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
189 | mode | ||
190 | }) | ||
191 | }) | ||
192 | |||
193 | it('Should fail with a password protected privacy and duplicated passwords', async function () { | ||
194 | const videoPasswords = [ 'password', 'password' ] | ||
195 | |||
196 | await checkVideoPasswordOptions({ | ||
197 | server, | ||
198 | token: server.accessToken, | ||
199 | videoPasswords, | ||
200 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
201 | mode | ||
202 | }) | ||
203 | }) | ||
204 | |||
205 | if (mode === 'updatePasswords') { | ||
206 | it('Should fail for an unauthenticated user', async function () { | ||
207 | const videoPasswords = [ 'password' ] | ||
208 | await checkVideoPasswordOptions({ | ||
209 | server, | ||
210 | token: null, | ||
211 | videoPasswords, | ||
212 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401, | ||
213 | mode | ||
214 | }) | ||
215 | }) | ||
216 | |||
217 | it('Should fail for an unauthorized user', async function () { | ||
218 | const videoPasswords = [ 'password' ] | ||
219 | await checkVideoPasswordOptions({ | ||
220 | server, | ||
221 | token: userAccessToken, | ||
222 | videoPasswords, | ||
223 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
224 | mode | ||
225 | }) | ||
226 | }) | ||
227 | } | ||
228 | |||
229 | it('Should succeed with a password protected privacy and correct passwords', async function () { | ||
230 | const videoPasswords = [ 'password1', 'password2' ] | ||
231 | const expectedStatus = mode === 'updatePasswords' || mode === 'updateVideo' | ||
232 | ? HttpStatusCode.NO_CONTENT_204 | ||
233 | : HttpStatusCode.OK_200 | ||
234 | |||
235 | await checkVideoPasswordOptions({ server, token: server.accessToken, videoPasswords, expectedStatus, mode }) | ||
236 | }) | ||
237 | } | ||
238 | |||
239 | describe('When adding or updating a video', function () { | ||
240 | describe('Resumable upload', function () { | ||
241 | validateVideoPasswordList('uploadResumable') | ||
242 | }) | ||
243 | |||
244 | describe('Legacy upload', function () { | ||
245 | validateVideoPasswordList('uploadLegacy') | ||
246 | }) | ||
247 | |||
248 | describe('When importing a video', function () { | ||
249 | validateVideoPasswordList('import') | ||
250 | }) | ||
251 | |||
252 | describe('When updating a video', function () { | ||
253 | validateVideoPasswordList('updateVideo') | ||
254 | }) | ||
255 | |||
256 | describe('When updating the password list of a video', function () { | ||
257 | validateVideoPasswordList('updatePasswords') | ||
258 | }) | ||
259 | |||
260 | describe('When creating a live', function () { | ||
261 | validateVideoPasswordList('live') | ||
262 | }) | ||
263 | }) | ||
264 | |||
265 | async function checkVideoAccessOptions (options: { | ||
266 | server: PeerTubeServer | ||
267 | token?: string | ||
268 | videoPassword?: string | ||
269 | expectedStatus: HttpStatusCodeType | ||
270 | mode: 'get' | 'getWithPassword' | 'getWithToken' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token' | ||
271 | }) { | ||
272 | const { server, token = null, videoPassword, expectedStatus, mode } = options | ||
273 | |||
274 | if (mode === 'get') { | ||
275 | return server.videos.get({ id: video.id, expectedStatus }) | ||
276 | } | ||
277 | |||
278 | if (mode === 'getWithToken') { | ||
279 | return server.videos.getWithToken({ | ||
280 | id: video.id, | ||
281 | token, | ||
282 | expectedStatus | ||
283 | }) | ||
284 | } | ||
285 | |||
286 | if (mode === 'getWithPassword') { | ||
287 | return server.videos.getWithPassword({ | ||
288 | id: video.id, | ||
289 | token, | ||
290 | expectedStatus, | ||
291 | password: videoPassword | ||
292 | }) | ||
293 | } | ||
294 | |||
295 | if (mode === 'rate') { | ||
296 | return server.videos.rate({ | ||
297 | id: video.id, | ||
298 | token, | ||
299 | expectedStatus, | ||
300 | rating: 'like', | ||
301 | videoPassword | ||
302 | }) | ||
303 | } | ||
304 | |||
305 | if (mode === 'createThread') { | ||
306 | const fields = { text: 'super comment' } | ||
307 | const headers = videoPassword !== undefined && videoPassword !== null | ||
308 | ? { 'x-peertube-video-password': videoPassword } | ||
309 | : undefined | ||
310 | const body = await makePostBodyRequest({ | ||
311 | url: server.url, | ||
312 | path: path + video.uuid + '/comment-threads', | ||
313 | token, | ||
314 | fields, | ||
315 | headers, | ||
316 | expectedStatus | ||
317 | }) | ||
318 | return JSON.parse(body.text) | ||
319 | } | ||
320 | |||
321 | if (mode === 'replyThread') { | ||
322 | const fields = { text: 'super reply' } | ||
323 | const headers = videoPassword !== undefined && videoPassword !== null | ||
324 | ? { 'x-peertube-video-password': videoPassword } | ||
325 | : undefined | ||
326 | return makePostBodyRequest({ | ||
327 | url: server.url, | ||
328 | path: path + video.uuid + '/comments/' + commentId, | ||
329 | token, | ||
330 | fields, | ||
331 | headers, | ||
332 | expectedStatus | ||
333 | }) | ||
334 | } | ||
335 | if (mode === 'listThreads') { | ||
336 | return server.comments.listThreads({ | ||
337 | videoId: video.id, | ||
338 | token, | ||
339 | expectedStatus, | ||
340 | videoPassword | ||
341 | }) | ||
342 | } | ||
343 | |||
344 | if (mode === 'listCaptions') { | ||
345 | return server.captions.list({ | ||
346 | videoId: video.id, | ||
347 | token, | ||
348 | expectedStatus, | ||
349 | videoPassword | ||
350 | }) | ||
351 | } | ||
352 | |||
353 | if (mode === 'token') { | ||
354 | return server.videoToken.create({ | ||
355 | videoId: video.id, | ||
356 | token, | ||
357 | expectedStatus, | ||
358 | videoPassword | ||
359 | }) | ||
360 | } | ||
361 | } | ||
362 | |||
363 | function checkVideoError (error: any, mode: 'providePassword' | 'incorrectPassword') { | ||
364 | const serverCode = mode === 'providePassword' | ||
365 | ? ServerErrorCode.VIDEO_REQUIRES_PASSWORD | ||
366 | : ServerErrorCode.INCORRECT_VIDEO_PASSWORD | ||
367 | |||
368 | const message = mode === 'providePassword' | ||
369 | ? 'Please provide a password to access this password protected video' | ||
370 | : 'Incorrect video password. Access to the video is denied.' | ||
371 | |||
372 | if (!error.code) { | ||
373 | error = JSON.parse(error.text) | ||
374 | } | ||
375 | |||
376 | expect(error.code).to.equal(serverCode) | ||
377 | expect(error.detail).to.equal(message) | ||
378 | expect(error.error).to.equal(message) | ||
379 | |||
380 | expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
381 | } | ||
382 | |||
383 | function validateVideoAccess (mode: 'get' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token') { | ||
384 | const requiresUserAuth = [ 'createThread', 'replyThread', 'rate' ].includes(mode) | ||
385 | let tokens: string[] | ||
386 | if (!requiresUserAuth) { | ||
387 | it('Should fail without providing a password for an unlogged user', async function () { | ||
388 | const body = await checkVideoAccessOptions({ server, expectedStatus: HttpStatusCode.FORBIDDEN_403, mode }) | ||
389 | const error = body as unknown as PeerTubeProblemDocument | ||
390 | |||
391 | checkVideoError(error, 'providePassword') | ||
392 | }) | ||
393 | } | ||
394 | |||
395 | it('Should fail without providing a password for an unauthorised user', async function () { | ||
396 | const tmp = mode === 'get' ? 'getWithToken' : mode | ||
397 | |||
398 | const body = await checkVideoAccessOptions({ | ||
399 | server, | ||
400 | token: userAccessToken, | ||
401 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
402 | mode: tmp | ||
403 | }) | ||
404 | |||
405 | const error = body as unknown as PeerTubeProblemDocument | ||
406 | |||
407 | checkVideoError(error, 'providePassword') | ||
408 | }) | ||
409 | |||
410 | it('Should fail if a wrong password is entered', async function () { | ||
411 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
412 | tokens = [ userAccessToken, server.accessToken ] | ||
413 | |||
414 | if (!requiresUserAuth) tokens.push(null) | ||
415 | |||
416 | for (const token of tokens) { | ||
417 | const body = await checkVideoAccessOptions({ | ||
418 | server, | ||
419 | token, | ||
420 | videoPassword: 'toto', | ||
421 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
422 | mode: tmp | ||
423 | }) | ||
424 | const error = body as unknown as PeerTubeProblemDocument | ||
425 | |||
426 | checkVideoError(error, 'incorrectPassword') | ||
427 | } | ||
428 | }) | ||
429 | |||
430 | it('Should fail if an empty password is entered', async function () { | ||
431 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
432 | |||
433 | for (const token of tokens) { | ||
434 | const body = await checkVideoAccessOptions({ | ||
435 | server, | ||
436 | token, | ||
437 | videoPassword: '', | ||
438 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
439 | mode: tmp | ||
440 | }) | ||
441 | const error = body as unknown as PeerTubeProblemDocument | ||
442 | |||
443 | checkVideoError(error, 'incorrectPassword') | ||
444 | } | ||
445 | }) | ||
446 | |||
447 | it('Should fail if an inccorect password containing the correct password is entered', async function () { | ||
448 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
449 | |||
450 | for (const token of tokens) { | ||
451 | const body = await checkVideoAccessOptions({ | ||
452 | server, | ||
453 | token, | ||
454 | videoPassword: 'password11', | ||
455 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
456 | mode: tmp | ||
457 | }) | ||
458 | const error = body as unknown as PeerTubeProblemDocument | ||
459 | |||
460 | checkVideoError(error, 'incorrectPassword') | ||
461 | } | ||
462 | }) | ||
463 | |||
464 | it('Should succeed without providing a password for an authorised user', async function () { | ||
465 | const tmp = mode === 'get' ? 'getWithToken' : mode | ||
466 | const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200 | ||
467 | |||
468 | const body = await checkVideoAccessOptions({ server, token: server.accessToken, expectedStatus, mode: tmp }) | ||
469 | |||
470 | if (mode === 'createThread') commentId = body.comment.id | ||
471 | }) | ||
472 | |||
473 | it('Should succeed using correct passwords', async function () { | ||
474 | const tmp = mode === 'get' ? 'getWithPassword' : mode | ||
475 | const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200 | ||
476 | |||
477 | for (const token of tokens) { | ||
478 | await checkVideoAccessOptions({ server, videoPassword: 'password1', token, expectedStatus, mode: tmp }) | ||
479 | await checkVideoAccessOptions({ server, videoPassword: 'password2', token, expectedStatus, mode: tmp }) | ||
480 | } | ||
481 | }) | ||
482 | } | ||
483 | |||
484 | describe('When accessing password protected video', function () { | ||
485 | |||
486 | describe('For getting a password protected video', function () { | ||
487 | validateVideoAccess('get') | ||
488 | }) | ||
489 | |||
490 | describe('For rating a video', function () { | ||
491 | validateVideoAccess('rate') | ||
492 | }) | ||
493 | |||
494 | describe('For creating a thread', function () { | ||
495 | validateVideoAccess('createThread') | ||
496 | }) | ||
497 | |||
498 | describe('For replying to a thread', function () { | ||
499 | validateVideoAccess('replyThread') | ||
500 | }) | ||
501 | |||
502 | describe('For listing threads', function () { | ||
503 | validateVideoAccess('listThreads') | ||
504 | }) | ||
505 | |||
506 | describe('For getting captions', function () { | ||
507 | validateVideoAccess('listCaptions') | ||
508 | }) | ||
509 | |||
510 | describe('For creating video file token', function () { | ||
511 | validateVideoAccess('token') | ||
512 | }) | ||
513 | }) | ||
514 | |||
515 | describe('When listing passwords', function () { | ||
516 | it('Should fail with a bad start pagination', async function () { | ||
517 | await checkBadStartPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
518 | }) | ||
519 | |||
520 | it('Should fail with a bad count pagination', async function () { | ||
521 | await checkBadCountPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
522 | }) | ||
523 | |||
524 | it('Should fail with an incorrect sort', async function () { | ||
525 | await checkBadSortPagination(server.url, path + video.uuid + '/passwords', server.accessToken) | ||
526 | }) | ||
527 | |||
528 | it('Should fail for unauthenticated user', async function () { | ||
529 | await server.videoPasswords.list({ | ||
530 | token: null, | ||
531 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401, | ||
532 | videoId: video.id | ||
533 | }) | ||
534 | }) | ||
535 | |||
536 | it('Should fail for unauthorized user', async function () { | ||
537 | await server.videoPasswords.list({ | ||
538 | token: userAccessToken, | ||
539 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
540 | videoId: video.id | ||
541 | }) | ||
542 | }) | ||
543 | |||
544 | it('Should succeed with the correct parameters', async function () { | ||
545 | await server.videoPasswords.list({ | ||
546 | token: server.accessToken, | ||
547 | expectedStatus: HttpStatusCode.OK_200, | ||
548 | videoId: video.id | ||
549 | }) | ||
550 | }) | ||
551 | }) | ||
552 | |||
553 | describe('When deleting a password', async function () { | ||
554 | const passwords = (await server.videoPasswords.list({ videoId: video.id })).data | ||
555 | |||
556 | it('Should fail with wrong password id', async function () { | ||
557 | await server.videoPasswords.remove({ id: -1, videoId: video.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
558 | }) | ||
559 | |||
560 | it('Should fail for unauthenticated user', async function () { | ||
561 | await server.videoPasswords.remove({ | ||
562 | id: passwords[0].id, | ||
563 | token: null, | ||
564 | videoId: video.id, | ||
565 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
566 | }) | ||
567 | }) | ||
568 | |||
569 | it('Should fail for unauthorized user', async function () { | ||
570 | await server.videoPasswords.remove({ | ||
571 | id: passwords[0].id, | ||
572 | token: userAccessToken, | ||
573 | videoId: video.id, | ||
574 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
575 | }) | ||
576 | }) | ||
577 | |||
578 | it('Should fail for non password protected video', async function () { | ||
579 | publicVideo = await server.videos.quickUpload({ name: 'public video' }) | ||
580 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: publicVideo.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
581 | }) | ||
582 | |||
583 | it('Should fail for password not linked to correct video', async function () { | ||
584 | const video2 = await server.videos.quickUpload({ | ||
585 | name: 'password protected video', | ||
586 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
587 | videoPasswords: [ 'password1', 'password2' ] | ||
588 | }) | ||
589 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: video2.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
590 | }) | ||
591 | |||
592 | it('Should succeed with correct parameter', async function () { | ||
593 | await server.videoPasswords.remove({ id: passwords[0].id, videoId: video.id, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
594 | }) | ||
595 | |||
596 | it('Should fail for last password of a video', async function () { | ||
597 | await server.videoPasswords.remove({ id: passwords[1].id, videoId: video.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
598 | }) | ||
599 | }) | ||
600 | |||
601 | after(async function () { | ||
602 | await cleanupTests([ server ]) | ||
603 | }) | ||
604 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-playlists.ts b/packages/tests/src/api/check-params/video-playlists.ts new file mode 100644 index 000000000..7f5be18d4 --- /dev/null +++ b/packages/tests/src/api/check-params/video-playlists.ts | |||
@@ -0,0 +1,695 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { | ||
5 | HttpStatusCode, | ||
6 | VideoPlaylistCreate, | ||
7 | VideoPlaylistCreateResult, | ||
8 | VideoPlaylistElementCreate, | ||
9 | VideoPlaylistElementUpdate, | ||
10 | VideoPlaylistPrivacy, | ||
11 | VideoPlaylistReorder, | ||
12 | VideoPlaylistType | ||
13 | } from '@peertube/peertube-models' | ||
14 | import { | ||
15 | cleanupTests, | ||
16 | createSingleServer, | ||
17 | makeGetRequest, | ||
18 | PeerTubeServer, | ||
19 | PlaylistsCommand, | ||
20 | setAccessTokensToServers, | ||
21 | setDefaultVideoChannel | ||
22 | } from '@peertube/peertube-server-commands' | ||
23 | |||
24 | describe('Test video playlists API validator', function () { | ||
25 | let server: PeerTubeServer | ||
26 | let userAccessToken: string | ||
27 | |||
28 | let playlist: VideoPlaylistCreateResult | ||
29 | let privatePlaylistUUID: string | ||
30 | |||
31 | let watchLaterPlaylistId: number | ||
32 | let videoId: number | ||
33 | let elementId: number | ||
34 | |||
35 | let command: PlaylistsCommand | ||
36 | |||
37 | // --------------------------------------------------------------- | ||
38 | |||
39 | before(async function () { | ||
40 | this.timeout(30000) | ||
41 | |||
42 | server = await createSingleServer(1) | ||
43 | |||
44 | await setAccessTokensToServers([ server ]) | ||
45 | await setDefaultVideoChannel([ server ]) | ||
46 | |||
47 | userAccessToken = await server.users.generateUserAndToken('user1') | ||
48 | videoId = (await server.videos.quickUpload({ name: 'video 1' })).id | ||
49 | |||
50 | command = server.playlists | ||
51 | |||
52 | { | ||
53 | const { data } = await command.listByAccount({ | ||
54 | token: server.accessToken, | ||
55 | handle: 'root', | ||
56 | start: 0, | ||
57 | count: 5, | ||
58 | playlistType: VideoPlaylistType.WATCH_LATER | ||
59 | }) | ||
60 | watchLaterPlaylistId = data[0].id | ||
61 | } | ||
62 | |||
63 | { | ||
64 | playlist = await command.create({ | ||
65 | attributes: { | ||
66 | displayName: 'super playlist', | ||
67 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
68 | videoChannelId: server.store.channel.id | ||
69 | } | ||
70 | }) | ||
71 | } | ||
72 | |||
73 | { | ||
74 | const created = await command.create({ | ||
75 | attributes: { | ||
76 | displayName: 'private', | ||
77 | privacy: VideoPlaylistPrivacy.PRIVATE | ||
78 | } | ||
79 | }) | ||
80 | privatePlaylistUUID = created.uuid | ||
81 | } | ||
82 | }) | ||
83 | |||
84 | describe('When listing playlists', function () { | ||
85 | const globalPath = '/api/v1/video-playlists' | ||
86 | const accountPath = '/api/v1/accounts/root/video-playlists' | ||
87 | const videoChannelPath = '/api/v1/video-channels/root_channel/video-playlists' | ||
88 | |||
89 | it('Should fail with a bad start pagination', async function () { | ||
90 | await checkBadStartPagination(server.url, globalPath, server.accessToken) | ||
91 | await checkBadStartPagination(server.url, accountPath, server.accessToken) | ||
92 | await checkBadStartPagination(server.url, videoChannelPath, server.accessToken) | ||
93 | }) | ||
94 | |||
95 | it('Should fail with a bad count pagination', async function () { | ||
96 | await checkBadCountPagination(server.url, globalPath, server.accessToken) | ||
97 | await checkBadCountPagination(server.url, accountPath, server.accessToken) | ||
98 | await checkBadCountPagination(server.url, videoChannelPath, server.accessToken) | ||
99 | }) | ||
100 | |||
101 | it('Should fail with an incorrect sort', async function () { | ||
102 | await checkBadSortPagination(server.url, globalPath, server.accessToken) | ||
103 | await checkBadSortPagination(server.url, accountPath, server.accessToken) | ||
104 | await checkBadSortPagination(server.url, videoChannelPath, server.accessToken) | ||
105 | }) | ||
106 | |||
107 | it('Should fail with a bad playlist type', async function () { | ||
108 | await makeGetRequest({ url: server.url, path: globalPath, query: { playlistType: 3 } }) | ||
109 | await makeGetRequest({ url: server.url, path: accountPath, query: { playlistType: 3 } }) | ||
110 | await makeGetRequest({ url: server.url, path: videoChannelPath, query: { playlistType: 3 } }) | ||
111 | }) | ||
112 | |||
113 | it('Should fail with a bad account parameter', async function () { | ||
114 | const accountPath = '/api/v1/accounts/root2/video-playlists' | ||
115 | |||
116 | await makeGetRequest({ | ||
117 | url: server.url, | ||
118 | path: accountPath, | ||
119 | expectedStatus: HttpStatusCode.NOT_FOUND_404, | ||
120 | token: server.accessToken | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with a bad video channel parameter', async function () { | ||
125 | const accountPath = '/api/v1/video-channels/bad_channel/video-playlists' | ||
126 | |||
127 | await makeGetRequest({ | ||
128 | url: server.url, | ||
129 | path: accountPath, | ||
130 | expectedStatus: HttpStatusCode.NOT_FOUND_404, | ||
131 | token: server.accessToken | ||
132 | }) | ||
133 | }) | ||
134 | |||
135 | it('Should success with the correct parameters', async function () { | ||
136 | await makeGetRequest({ url: server.url, path: globalPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken }) | ||
137 | await makeGetRequest({ url: server.url, path: accountPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken }) | ||
138 | await makeGetRequest({ | ||
139 | url: server.url, | ||
140 | path: videoChannelPath, | ||
141 | expectedStatus: HttpStatusCode.OK_200, | ||
142 | token: server.accessToken | ||
143 | }) | ||
144 | }) | ||
145 | }) | ||
146 | |||
147 | describe('When listing videos of a playlist', function () { | ||
148 | const path = '/api/v1/video-playlists/' | ||
149 | |||
150 | it('Should fail with a bad start pagination', async function () { | ||
151 | await checkBadStartPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken) | ||
152 | }) | ||
153 | |||
154 | it('Should fail with a bad count pagination', async function () { | ||
155 | await checkBadCountPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken) | ||
156 | }) | ||
157 | |||
158 | it('Should success with the correct parameters', async function () { | ||
159 | await makeGetRequest({ url: server.url, path: path + playlist.shortUUID + '/videos', expectedStatus: HttpStatusCode.OK_200 }) | ||
160 | }) | ||
161 | }) | ||
162 | |||
163 | describe('When getting a video playlist', function () { | ||
164 | it('Should fail with a bad id or uuid', async function () { | ||
165 | await command.get({ playlistId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
166 | }) | ||
167 | |||
168 | it('Should fail with an unknown playlist', async function () { | ||
169 | await command.get({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
170 | }) | ||
171 | |||
172 | it('Should fail to get an unlisted playlist with the number id', async function () { | ||
173 | const playlist = await command.create({ | ||
174 | attributes: { | ||
175 | displayName: 'super playlist', | ||
176 | videoChannelId: server.store.channel.id, | ||
177 | privacy: VideoPlaylistPrivacy.UNLISTED | ||
178 | } | ||
179 | }) | ||
180 | |||
181 | await command.get({ playlistId: playlist.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
182 | await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 }) | ||
183 | }) | ||
184 | |||
185 | it('Should succeed with the correct params', async function () { | ||
186 | await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 }) | ||
187 | }) | ||
188 | }) | ||
189 | |||
190 | describe('When creating/updating a video playlist', function () { | ||
191 | const getBase = ( | ||
192 | attributes?: Partial<VideoPlaylistCreate>, | ||
193 | wrapper?: Partial<Parameters<PlaylistsCommand['create']>[0]> | ||
194 | ) => { | ||
195 | return { | ||
196 | attributes: { | ||
197 | displayName: 'display name', | ||
198 | privacy: VideoPlaylistPrivacy.UNLISTED, | ||
199 | thumbnailfile: 'custom-thumbnail.jpg', | ||
200 | videoChannelId: server.store.channel.id, | ||
201 | |||
202 | ...attributes | ||
203 | }, | ||
204 | |||
205 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
206 | |||
207 | ...wrapper | ||
208 | } | ||
209 | } | ||
210 | const getUpdate = (params: any, playlistId: number | string) => { | ||
211 | return { ...params, playlistId } | ||
212 | } | ||
213 | |||
214 | it('Should fail with an unauthenticated user', async function () { | ||
215 | const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
216 | |||
217 | await command.create(params) | ||
218 | await command.update(getUpdate(params, playlist.shortUUID)) | ||
219 | }) | ||
220 | |||
221 | it('Should fail without displayName', async function () { | ||
222 | const params = getBase({ displayName: undefined }) | ||
223 | |||
224 | await command.create(params) | ||
225 | }) | ||
226 | |||
227 | it('Should fail with an incorrect display name', async function () { | ||
228 | const params = getBase({ displayName: 's'.repeat(300) }) | ||
229 | |||
230 | await command.create(params) | ||
231 | await command.update(getUpdate(params, playlist.shortUUID)) | ||
232 | }) | ||
233 | |||
234 | it('Should fail with an incorrect description', async function () { | ||
235 | const params = getBase({ description: 't' }) | ||
236 | |||
237 | await command.create(params) | ||
238 | await command.update(getUpdate(params, playlist.shortUUID)) | ||
239 | }) | ||
240 | |||
241 | it('Should fail with an incorrect privacy', async function () { | ||
242 | const params = getBase({ privacy: 45 as any }) | ||
243 | |||
244 | await command.create(params) | ||
245 | await command.update(getUpdate(params, playlist.shortUUID)) | ||
246 | }) | ||
247 | |||
248 | it('Should fail with an unknown video channel id', async function () { | ||
249 | const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
250 | |||
251 | await command.create(params) | ||
252 | await command.update(getUpdate(params, playlist.shortUUID)) | ||
253 | }) | ||
254 | |||
255 | it('Should fail with an incorrect thumbnail file', async function () { | ||
256 | const params = getBase({ thumbnailfile: 'video_short.mp4' }) | ||
257 | |||
258 | await command.create(params) | ||
259 | await command.update(getUpdate(params, playlist.shortUUID)) | ||
260 | }) | ||
261 | |||
262 | it('Should fail with a thumbnail file too big', async function () { | ||
263 | const params = getBase({ thumbnailfile: 'custom-preview-big.png' }) | ||
264 | |||
265 | await command.create(params) | ||
266 | await command.update(getUpdate(params, playlist.shortUUID)) | ||
267 | }) | ||
268 | |||
269 | it('Should fail to set "public" a playlist not assigned to a channel', async function () { | ||
270 | const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: undefined }) | ||
271 | const params2 = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: 'null' as any }) | ||
272 | const params3 = getBase({ privacy: undefined, videoChannelId: 'null' as any }) | ||
273 | |||
274 | await command.create(params) | ||
275 | await command.create(params2) | ||
276 | await command.update(getUpdate(params, privatePlaylistUUID)) | ||
277 | await command.update(getUpdate(params2, playlist.shortUUID)) | ||
278 | await command.update(getUpdate(params3, playlist.shortUUID)) | ||
279 | }) | ||
280 | |||
281 | it('Should fail with an unknown playlist to update', async function () { | ||
282 | await command.update(getUpdate( | ||
283 | getBase({}, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }), | ||
284 | 42 | ||
285 | )) | ||
286 | }) | ||
287 | |||
288 | it('Should fail to update a playlist of another user', async function () { | ||
289 | await command.update(getUpdate( | ||
290 | getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }), | ||
291 | playlist.shortUUID | ||
292 | )) | ||
293 | }) | ||
294 | |||
295 | it('Should fail to update the watch later playlist', async function () { | ||
296 | await command.update(getUpdate( | ||
297 | getBase({}, { expectedStatus: HttpStatusCode.BAD_REQUEST_400 }), | ||
298 | watchLaterPlaylistId | ||
299 | )) | ||
300 | }) | ||
301 | |||
302 | it('Should succeed with the correct params', async function () { | ||
303 | { | ||
304 | const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 }) | ||
305 | await command.create(params) | ||
306 | } | ||
307 | |||
308 | { | ||
309 | const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
310 | await command.update(getUpdate(params, playlist.shortUUID)) | ||
311 | } | ||
312 | }) | ||
313 | }) | ||
314 | |||
315 | describe('When adding an element in a playlist', function () { | ||
316 | const getBase = ( | ||
317 | attributes?: Partial<VideoPlaylistElementCreate>, | ||
318 | wrapper?: Partial<Parameters<PlaylistsCommand['addElement']>[0]> | ||
319 | ) => { | ||
320 | return { | ||
321 | attributes: { | ||
322 | videoId, | ||
323 | startTimestamp: 2, | ||
324 | stopTimestamp: 3, | ||
325 | |||
326 | ...attributes | ||
327 | }, | ||
328 | |||
329 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
330 | playlistId: playlist.id, | ||
331 | |||
332 | ...wrapper | ||
333 | } | ||
334 | } | ||
335 | |||
336 | it('Should fail with an unauthenticated user', async function () { | ||
337 | const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
338 | await command.addElement(params) | ||
339 | }) | ||
340 | |||
341 | it('Should fail with the playlist of another user', async function () { | ||
342 | const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
343 | await command.addElement(params) | ||
344 | }) | ||
345 | |||
346 | it('Should fail with an unknown or incorrect playlist id', async function () { | ||
347 | { | ||
348 | const params = getBase({}, { playlistId: 'toto' }) | ||
349 | await command.addElement(params) | ||
350 | } | ||
351 | |||
352 | { | ||
353 | const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
354 | await command.addElement(params) | ||
355 | } | ||
356 | }) | ||
357 | |||
358 | it('Should fail with an unknown or incorrect video id', async function () { | ||
359 | const params = getBase({ videoId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
360 | await command.addElement(params) | ||
361 | }) | ||
362 | |||
363 | it('Should fail with a bad start/stop timestamp', async function () { | ||
364 | { | ||
365 | const params = getBase({ startTimestamp: -42 }) | ||
366 | await command.addElement(params) | ||
367 | } | ||
368 | |||
369 | { | ||
370 | const params = getBase({ stopTimestamp: 'toto' as any }) | ||
371 | await command.addElement(params) | ||
372 | } | ||
373 | }) | ||
374 | |||
375 | it('Succeed with the correct params', async function () { | ||
376 | const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 }) | ||
377 | const created = await command.addElement(params) | ||
378 | elementId = created.id | ||
379 | }) | ||
380 | }) | ||
381 | |||
382 | describe('When updating an element in a playlist', function () { | ||
383 | const getBase = ( | ||
384 | attributes?: Partial<VideoPlaylistElementUpdate>, | ||
385 | wrapper?: Partial<Parameters<PlaylistsCommand['updateElement']>[0]> | ||
386 | ) => { | ||
387 | return { | ||
388 | attributes: { | ||
389 | startTimestamp: 1, | ||
390 | stopTimestamp: 2, | ||
391 | |||
392 | ...attributes | ||
393 | }, | ||
394 | |||
395 | elementId, | ||
396 | playlistId: playlist.id, | ||
397 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
398 | |||
399 | ...wrapper | ||
400 | } | ||
401 | } | ||
402 | |||
403 | it('Should fail with an unauthenticated user', async function () { | ||
404 | const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
405 | await command.updateElement(params) | ||
406 | }) | ||
407 | |||
408 | it('Should fail with the playlist of another user', async function () { | ||
409 | const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
410 | await command.updateElement(params) | ||
411 | }) | ||
412 | |||
413 | it('Should fail with an unknown or incorrect playlist id', async function () { | ||
414 | { | ||
415 | const params = getBase({}, { playlistId: 'toto' }) | ||
416 | await command.updateElement(params) | ||
417 | } | ||
418 | |||
419 | { | ||
420 | const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
421 | await command.updateElement(params) | ||
422 | } | ||
423 | }) | ||
424 | |||
425 | it('Should fail with an unknown or incorrect playlistElement id', async function () { | ||
426 | { | ||
427 | const params = getBase({}, { elementId: 'toto' }) | ||
428 | await command.updateElement(params) | ||
429 | } | ||
430 | |||
431 | { | ||
432 | const params = getBase({}, { elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
433 | await command.updateElement(params) | ||
434 | } | ||
435 | }) | ||
436 | |||
437 | it('Should fail with a bad start/stop timestamp', async function () { | ||
438 | { | ||
439 | const params = getBase({ startTimestamp: 'toto' as any }) | ||
440 | await command.updateElement(params) | ||
441 | } | ||
442 | |||
443 | { | ||
444 | const params = getBase({ stopTimestamp: -42 }) | ||
445 | await command.updateElement(params) | ||
446 | } | ||
447 | }) | ||
448 | |||
449 | it('Should fail with an unknown element', async function () { | ||
450 | const params = getBase({}, { elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
451 | await command.updateElement(params) | ||
452 | }) | ||
453 | |||
454 | it('Succeed with the correct params', async function () { | ||
455 | const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
456 | await command.updateElement(params) | ||
457 | }) | ||
458 | }) | ||
459 | |||
460 | describe('When reordering elements of a playlist', function () { | ||
461 | let videoId3: number | ||
462 | let videoId4: number | ||
463 | |||
464 | const getBase = ( | ||
465 | attributes?: Partial<VideoPlaylistReorder>, | ||
466 | wrapper?: Partial<Parameters<PlaylistsCommand['reorderElements']>[0]> | ||
467 | ) => { | ||
468 | return { | ||
469 | attributes: { | ||
470 | startPosition: 1, | ||
471 | insertAfterPosition: 2, | ||
472 | reorderLength: 3, | ||
473 | |||
474 | ...attributes | ||
475 | }, | ||
476 | |||
477 | playlistId: playlist.shortUUID, | ||
478 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
479 | |||
480 | ...wrapper | ||
481 | } | ||
482 | } | ||
483 | |||
484 | before(async function () { | ||
485 | videoId3 = (await server.videos.quickUpload({ name: 'video 3' })).id | ||
486 | videoId4 = (await server.videos.quickUpload({ name: 'video 4' })).id | ||
487 | |||
488 | for (const id of [ videoId3, videoId4 ]) { | ||
489 | await command.addElement({ playlistId: playlist.shortUUID, attributes: { videoId: id } }) | ||
490 | } | ||
491 | }) | ||
492 | |||
493 | it('Should fail with an unauthenticated user', async function () { | ||
494 | const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
495 | await command.reorderElements(params) | ||
496 | }) | ||
497 | |||
498 | it('Should fail with the playlist of another user', async function () { | ||
499 | const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
500 | await command.reorderElements(params) | ||
501 | }) | ||
502 | |||
503 | it('Should fail with an invalid playlist', async function () { | ||
504 | { | ||
505 | const params = getBase({}, { playlistId: 'toto' }) | ||
506 | await command.reorderElements(params) | ||
507 | } | ||
508 | |||
509 | { | ||
510 | const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
511 | await command.reorderElements(params) | ||
512 | } | ||
513 | }) | ||
514 | |||
515 | it('Should fail with an invalid start position', async function () { | ||
516 | { | ||
517 | const params = getBase({ startPosition: -1 }) | ||
518 | await command.reorderElements(params) | ||
519 | } | ||
520 | |||
521 | { | ||
522 | const params = getBase({ startPosition: 'toto' as any }) | ||
523 | await command.reorderElements(params) | ||
524 | } | ||
525 | |||
526 | { | ||
527 | const params = getBase({ startPosition: 42 }) | ||
528 | await command.reorderElements(params) | ||
529 | } | ||
530 | }) | ||
531 | |||
532 | it('Should fail with an invalid insert after position', async function () { | ||
533 | { | ||
534 | const params = getBase({ insertAfterPosition: 'toto' as any }) | ||
535 | await command.reorderElements(params) | ||
536 | } | ||
537 | |||
538 | { | ||
539 | const params = getBase({ insertAfterPosition: -2 }) | ||
540 | await command.reorderElements(params) | ||
541 | } | ||
542 | |||
543 | { | ||
544 | const params = getBase({ insertAfterPosition: 42 }) | ||
545 | await command.reorderElements(params) | ||
546 | } | ||
547 | }) | ||
548 | |||
549 | it('Should fail with an invalid reorder length', async function () { | ||
550 | { | ||
551 | const params = getBase({ reorderLength: 'toto' as any }) | ||
552 | await command.reorderElements(params) | ||
553 | } | ||
554 | |||
555 | { | ||
556 | const params = getBase({ reorderLength: -2 }) | ||
557 | await command.reorderElements(params) | ||
558 | } | ||
559 | |||
560 | { | ||
561 | const params = getBase({ reorderLength: 42 }) | ||
562 | await command.reorderElements(params) | ||
563 | } | ||
564 | }) | ||
565 | |||
566 | it('Succeed with the correct params', async function () { | ||
567 | const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
568 | await command.reorderElements(params) | ||
569 | }) | ||
570 | }) | ||
571 | |||
572 | describe('When checking exists in playlist endpoint', function () { | ||
573 | const path = '/api/v1/users/me/video-playlists/videos-exist' | ||
574 | |||
575 | it('Should fail with an unauthenticated user', async function () { | ||
576 | await makeGetRequest({ | ||
577 | url: server.url, | ||
578 | path, | ||
579 | query: { videoIds: [ 1, 2 ] }, | ||
580 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
581 | }) | ||
582 | }) | ||
583 | |||
584 | it('Should fail with invalid video ids', async function () { | ||
585 | await makeGetRequest({ | ||
586 | url: server.url, | ||
587 | token: server.accessToken, | ||
588 | path, | ||
589 | query: { videoIds: 'toto' } | ||
590 | }) | ||
591 | |||
592 | await makeGetRequest({ | ||
593 | url: server.url, | ||
594 | token: server.accessToken, | ||
595 | path, | ||
596 | query: { videoIds: [ 'toto' ] } | ||
597 | }) | ||
598 | |||
599 | await makeGetRequest({ | ||
600 | url: server.url, | ||
601 | token: server.accessToken, | ||
602 | path, | ||
603 | query: { videoIds: [ 1, 'toto' ] } | ||
604 | }) | ||
605 | }) | ||
606 | |||
607 | it('Should succeed with the correct params', async function () { | ||
608 | await makeGetRequest({ | ||
609 | url: server.url, | ||
610 | token: server.accessToken, | ||
611 | path, | ||
612 | query: { videoIds: [ 1, 2 ] }, | ||
613 | expectedStatus: HttpStatusCode.OK_200 | ||
614 | }) | ||
615 | }) | ||
616 | }) | ||
617 | |||
618 | describe('When deleting an element in a playlist', function () { | ||
619 | const getBase = (wrapper: Partial<Parameters<PlaylistsCommand['removeElement']>[0]>) => { | ||
620 | return { | ||
621 | elementId, | ||
622 | playlistId: playlist.uuid, | ||
623 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
624 | |||
625 | ...wrapper | ||
626 | } | ||
627 | } | ||
628 | |||
629 | it('Should fail with an unauthenticated user', async function () { | ||
630 | const params = getBase({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
631 | await command.removeElement(params) | ||
632 | }) | ||
633 | |||
634 | it('Should fail with the playlist of another user', async function () { | ||
635 | const params = getBase({ token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
636 | await command.removeElement(params) | ||
637 | }) | ||
638 | |||
639 | it('Should fail with an unknown or incorrect playlist id', async function () { | ||
640 | { | ||
641 | const params = getBase({ playlistId: 'toto' }) | ||
642 | await command.removeElement(params) | ||
643 | } | ||
644 | |||
645 | { | ||
646 | const params = getBase({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
647 | await command.removeElement(params) | ||
648 | } | ||
649 | }) | ||
650 | |||
651 | it('Should fail with an unknown or incorrect video id', async function () { | ||
652 | { | ||
653 | const params = getBase({ elementId: 'toto' as any }) | ||
654 | await command.removeElement(params) | ||
655 | } | ||
656 | |||
657 | { | ||
658 | const params = getBase({ elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
659 | await command.removeElement(params) | ||
660 | } | ||
661 | }) | ||
662 | |||
663 | it('Should fail with an unknown element', async function () { | ||
664 | const params = getBase({ elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
665 | await command.removeElement(params) | ||
666 | }) | ||
667 | |||
668 | it('Succeed with the correct params', async function () { | ||
669 | const params = getBase({ expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
670 | await command.removeElement(params) | ||
671 | }) | ||
672 | }) | ||
673 | |||
674 | describe('When deleting a playlist', function () { | ||
675 | it('Should fail with an unknown playlist', async function () { | ||
676 | await command.delete({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
677 | }) | ||
678 | |||
679 | it('Should fail with a playlist of another user', async function () { | ||
680 | await command.delete({ token: userAccessToken, playlistId: playlist.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
681 | }) | ||
682 | |||
683 | it('Should fail with the watch later playlist', async function () { | ||
684 | await command.delete({ playlistId: watchLaterPlaylistId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
685 | }) | ||
686 | |||
687 | it('Should succeed with the correct params', async function () { | ||
688 | await command.delete({ playlistId: playlist.uuid }) | ||
689 | }) | ||
690 | }) | ||
691 | |||
692 | after(async function () { | ||
693 | await cleanupTests([ server ]) | ||
694 | }) | ||
695 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-source.ts b/packages/tests/src/api/check-params/video-source.ts new file mode 100644 index 000000000..918182b8d --- /dev/null +++ b/packages/tests/src/api/check-params/video-source.ts | |||
@@ -0,0 +1,154 @@ | |||
1 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
2 | import { | ||
3 | cleanupTests, | ||
4 | createSingleServer, | ||
5 | PeerTubeServer, | ||
6 | setAccessTokensToServers, | ||
7 | setDefaultVideoChannel, | ||
8 | waitJobs | ||
9 | } from '@peertube/peertube-server-commands' | ||
10 | |||
11 | describe('Test video sources API validator', function () { | ||
12 | let server: PeerTubeServer = null | ||
13 | let uuid: string | ||
14 | let userToken: string | ||
15 | |||
16 | before(async function () { | ||
17 | this.timeout(120000) | ||
18 | |||
19 | server = await createSingleServer(1) | ||
20 | await setAccessTokensToServers([ server ]) | ||
21 | await setDefaultVideoChannel([ server ]) | ||
22 | |||
23 | userToken = await server.users.generateUserAndToken('user1') | ||
24 | }) | ||
25 | |||
26 | describe('When getting latest source', function () { | ||
27 | |||
28 | before(async function () { | ||
29 | const created = await server.videos.quickUpload({ name: 'video' }) | ||
30 | uuid = created.uuid | ||
31 | }) | ||
32 | |||
33 | it('Should fail without a valid uuid', async function () { | ||
34 | await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
35 | }) | ||
36 | |||
37 | it('Should receive 404 when passing a non existing video id', async function () { | ||
38 | await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
39 | }) | ||
40 | |||
41 | it('Should not get the source as unauthenticated', async function () { | ||
42 | await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null }) | ||
43 | }) | ||
44 | |||
45 | it('Should not get the source with another user', async function () { | ||
46 | await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken }) | ||
47 | }) | ||
48 | |||
49 | it('Should succeed with the correct parameters get the source as another user', async function () { | ||
50 | await server.videos.getSource({ id: uuid }) | ||
51 | }) | ||
52 | }) | ||
53 | |||
54 | describe('When updating source video file', function () { | ||
55 | let userAccessToken: string | ||
56 | let userId: number | ||
57 | |||
58 | let videoId: string | ||
59 | let userVideoId: string | ||
60 | |||
61 | before(async function () { | ||
62 | const res = await server.users.generate('user2') | ||
63 | userAccessToken = res.token | ||
64 | userId = res.userId | ||
65 | |||
66 | const { uuid } = await server.videos.quickUpload({ name: 'video' }) | ||
67 | videoId = uuid | ||
68 | |||
69 | await waitJobs([ server ]) | ||
70 | }) | ||
71 | |||
72 | it('Should fail if not enabled on the instance', async function () { | ||
73 | await server.config.disableFileUpdate() | ||
74 | |||
75 | await server.videos.replaceSourceFile({ videoId, fixture: 'video_short.mp4', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
76 | }) | ||
77 | |||
78 | it('Should fail on an unknown video', async function () { | ||
79 | await server.config.enableFileUpdate() | ||
80 | |||
81 | await server.videos.replaceSourceFile({ videoId: 404, fixture: 'video_short.mp4', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
82 | }) | ||
83 | |||
84 | it('Should fail with an invalid video', async function () { | ||
85 | await server.config.enableLive({ allowReplay: false }) | ||
86 | |||
87 | const { video } = await server.live.quickCreate({ saveReplay: false, permanentLive: true }) | ||
88 | await server.videos.replaceSourceFile({ | ||
89 | videoId: video.uuid, | ||
90 | fixture: 'video_short.mp4', | ||
91 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
92 | }) | ||
93 | }) | ||
94 | |||
95 | it('Should fail without token', async function () { | ||
96 | await server.videos.replaceSourceFile({ | ||
97 | token: null, | ||
98 | videoId, | ||
99 | fixture: 'video_short.mp4', | ||
100 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
101 | }) | ||
102 | }) | ||
103 | |||
104 | it('Should fail with another user', async function () { | ||
105 | await server.videos.replaceSourceFile({ | ||
106 | token: userAccessToken, | ||
107 | videoId, | ||
108 | fixture: 'video_short.mp4', | ||
109 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
110 | }) | ||
111 | }) | ||
112 | |||
113 | it('Should fail with an incorrect input file', async function () { | ||
114 | await server.videos.replaceSourceFile({ | ||
115 | fixture: 'video_short_fake.webm', | ||
116 | videoId, | ||
117 | completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 | ||
118 | }) | ||
119 | |||
120 | await server.videos.replaceSourceFile({ | ||
121 | fixture: 'video_short.mkv', | ||
122 | videoId, | ||
123 | expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 | ||
124 | }) | ||
125 | }) | ||
126 | |||
127 | it('Should fail if quota is exceeded', async function () { | ||
128 | this.timeout(60000) | ||
129 | |||
130 | const { uuid } = await server.videos.quickUpload({ name: 'user video' }) | ||
131 | userVideoId = uuid | ||
132 | await waitJobs([ server ]) | ||
133 | |||
134 | await server.users.update({ userId, videoQuota: 1 }) | ||
135 | await server.videos.replaceSourceFile({ | ||
136 | token: userAccessToken, | ||
137 | videoId: uuid, | ||
138 | fixture: 'video_short.mp4', | ||
139 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
140 | }) | ||
141 | }) | ||
142 | |||
143 | it('Should succeed with the correct params', async function () { | ||
144 | this.timeout(60000) | ||
145 | |||
146 | await server.users.update({ userId, videoQuota: 1000 * 1000 * 1000 }) | ||
147 | await server.videos.replaceSourceFile({ videoId: userVideoId, fixture: 'video_short.mp4' }) | ||
148 | }) | ||
149 | }) | ||
150 | |||
151 | after(async function () { | ||
152 | await cleanupTests([ server ]) | ||
153 | }) | ||
154 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-storyboards.ts b/packages/tests/src/api/check-params/video-storyboards.ts new file mode 100644 index 000000000..f83b541d8 --- /dev/null +++ b/packages/tests/src/api/check-params/video-storyboards.ts | |||
@@ -0,0 +1,45 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
5 | |||
6 | describe('Test video storyboards API validator', function () { | ||
7 | let server: PeerTubeServer | ||
8 | |||
9 | let publicVideo: { uuid: string } | ||
10 | let privateVideo: { uuid: string } | ||
11 | |||
12 | // --------------------------------------------------------------- | ||
13 | |||
14 | before(async function () { | ||
15 | this.timeout(120000) | ||
16 | |||
17 | server = await createSingleServer(1) | ||
18 | await setAccessTokensToServers([ server ]) | ||
19 | |||
20 | publicVideo = await server.videos.quickUpload({ name: 'public' }) | ||
21 | privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE }) | ||
22 | }) | ||
23 | |||
24 | it('Should fail without a valid uuid', async function () { | ||
25 | await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
26 | }) | ||
27 | |||
28 | it('Should receive 404 when passing a non existing video id', async function () { | ||
29 | await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
30 | }) | ||
31 | |||
32 | it('Should not get the private storyboard without the appropriate token', async function () { | ||
33 | await server.storyboard.list({ id: privateVideo.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null }) | ||
34 | await server.storyboard.list({ id: publicVideo.uuid, expectedStatus: HttpStatusCode.OK_200, token: null }) | ||
35 | }) | ||
36 | |||
37 | it('Should succeed with the correct parameters', async function () { | ||
38 | await server.storyboard.list({ id: privateVideo.uuid }) | ||
39 | await server.storyboard.list({ id: publicVideo.uuid }) | ||
40 | }) | ||
41 | |||
42 | after(async function () { | ||
43 | await cleanupTests([ server ]) | ||
44 | }) | ||
45 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-studio.ts b/packages/tests/src/api/check-params/video-studio.ts new file mode 100644 index 000000000..ae83f3590 --- /dev/null +++ b/packages/tests/src/api/check-params/video-studio.ts | |||
@@ -0,0 +1,392 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode, HttpStatusCodeType, VideoStudioTask } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | PeerTubeServer, | ||
8 | setAccessTokensToServers, | ||
9 | VideoStudioCommand, | ||
10 | waitJobs | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test video studio API validator', function () { | ||
14 | let server: PeerTubeServer | ||
15 | let command: VideoStudioCommand | ||
16 | let userAccessToken: string | ||
17 | let videoUUID: string | ||
18 | |||
19 | // --------------------------------------------------------------- | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(120_000) | ||
23 | |||
24 | server = await createSingleServer(1) | ||
25 | |||
26 | await setAccessTokensToServers([ server ]) | ||
27 | userAccessToken = await server.users.generateUserAndToken('user1') | ||
28 | |||
29 | await server.config.enableMinimumTranscoding() | ||
30 | |||
31 | const { uuid } = await server.videos.quickUpload({ name: 'video' }) | ||
32 | videoUUID = uuid | ||
33 | |||
34 | command = server.videoStudio | ||
35 | |||
36 | await waitJobs([ server ]) | ||
37 | }) | ||
38 | |||
39 | describe('Task creation', function () { | ||
40 | |||
41 | describe('Config settings', function () { | ||
42 | |||
43 | it('Should fail if studio is disabled', async function () { | ||
44 | await server.config.updateExistingSubConfig({ | ||
45 | newConfig: { | ||
46 | videoStudio: { | ||
47 | enabled: false | ||
48 | } | ||
49 | } | ||
50 | }) | ||
51 | |||
52 | await command.createEditionTasks({ | ||
53 | videoId: videoUUID, | ||
54 | tasks: VideoStudioCommand.getComplexTask(), | ||
55 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
56 | }) | ||
57 | }) | ||
58 | |||
59 | it('Should fail to enable studio if transcoding is disabled', async function () { | ||
60 | await server.config.updateExistingSubConfig({ | ||
61 | newConfig: { | ||
62 | videoStudio: { | ||
63 | enabled: true | ||
64 | }, | ||
65 | transcoding: { | ||
66 | enabled: false | ||
67 | } | ||
68 | }, | ||
69 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
70 | }) | ||
71 | }) | ||
72 | |||
73 | it('Should succeed to enable video studio', async function () { | ||
74 | await server.config.updateExistingSubConfig({ | ||
75 | newConfig: { | ||
76 | videoStudio: { | ||
77 | enabled: true | ||
78 | }, | ||
79 | transcoding: { | ||
80 | enabled: true | ||
81 | } | ||
82 | } | ||
83 | }) | ||
84 | }) | ||
85 | }) | ||
86 | |||
87 | describe('Common tasks', function () { | ||
88 | |||
89 | it('Should fail without token', async function () { | ||
90 | await command.createEditionTasks({ | ||
91 | token: null, | ||
92 | videoId: videoUUID, | ||
93 | tasks: VideoStudioCommand.getComplexTask(), | ||
94 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
95 | }) | ||
96 | }) | ||
97 | |||
98 | it('Should fail with another user token', async function () { | ||
99 | await command.createEditionTasks({ | ||
100 | token: userAccessToken, | ||
101 | videoId: videoUUID, | ||
102 | tasks: VideoStudioCommand.getComplexTask(), | ||
103 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
104 | }) | ||
105 | }) | ||
106 | |||
107 | it('Should fail with an invalid video', async function () { | ||
108 | await command.createEditionTasks({ | ||
109 | videoId: 'tintin', | ||
110 | tasks: VideoStudioCommand.getComplexTask(), | ||
111 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
112 | }) | ||
113 | }) | ||
114 | |||
115 | it('Should fail with an unknown video', async function () { | ||
116 | await command.createEditionTasks({ | ||
117 | videoId: 42, | ||
118 | tasks: VideoStudioCommand.getComplexTask(), | ||
119 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
120 | }) | ||
121 | }) | ||
122 | |||
123 | it('Should fail with an already in transcoding state video', async function () { | ||
124 | this.timeout(60000) | ||
125 | |||
126 | const { uuid } = await server.videos.quickUpload({ name: 'transcoded video' }) | ||
127 | await waitJobs([ server ]) | ||
128 | |||
129 | await server.jobs.pauseJobQueue() | ||
130 | await server.videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' }) | ||
131 | |||
132 | await command.createEditionTasks({ | ||
133 | videoId: uuid, | ||
134 | tasks: VideoStudioCommand.getComplexTask(), | ||
135 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
136 | }) | ||
137 | |||
138 | await server.jobs.resumeJobQueue() | ||
139 | }) | ||
140 | |||
141 | it('Should fail with a bad complex task', async function () { | ||
142 | await command.createEditionTasks({ | ||
143 | videoId: videoUUID, | ||
144 | tasks: [ | ||
145 | { | ||
146 | name: 'cut', | ||
147 | options: { | ||
148 | start: 1, | ||
149 | end: 2 | ||
150 | } | ||
151 | }, | ||
152 | { | ||
153 | name: 'hadock', | ||
154 | options: { | ||
155 | start: 1, | ||
156 | end: 2 | ||
157 | } | ||
158 | } | ||
159 | ] as any, | ||
160 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
161 | }) | ||
162 | }) | ||
163 | |||
164 | it('Should fail without task', async function () { | ||
165 | await command.createEditionTasks({ | ||
166 | videoId: videoUUID, | ||
167 | tasks: [], | ||
168 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
169 | }) | ||
170 | }) | ||
171 | |||
172 | it('Should fail with too many tasks', async function () { | ||
173 | const tasks: VideoStudioTask[] = [] | ||
174 | |||
175 | for (let i = 0; i < 110; i++) { | ||
176 | tasks.push({ | ||
177 | name: 'cut', | ||
178 | options: { | ||
179 | start: 1 | ||
180 | } | ||
181 | }) | ||
182 | } | ||
183 | |||
184 | await command.createEditionTasks({ | ||
185 | videoId: videoUUID, | ||
186 | tasks, | ||
187 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
188 | }) | ||
189 | }) | ||
190 | |||
191 | it('Should succeed with correct parameters', async function () { | ||
192 | await server.jobs.pauseJobQueue() | ||
193 | |||
194 | await command.createEditionTasks({ | ||
195 | videoId: videoUUID, | ||
196 | tasks: VideoStudioCommand.getComplexTask(), | ||
197 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
198 | }) | ||
199 | }) | ||
200 | |||
201 | it('Should fail with a video that is already waiting for edition', async function () { | ||
202 | this.timeout(120000) | ||
203 | |||
204 | await command.createEditionTasks({ | ||
205 | videoId: videoUUID, | ||
206 | tasks: VideoStudioCommand.getComplexTask(), | ||
207 | expectedStatus: HttpStatusCode.CONFLICT_409 | ||
208 | }) | ||
209 | |||
210 | await server.jobs.resumeJobQueue() | ||
211 | |||
212 | await waitJobs([ server ]) | ||
213 | }) | ||
214 | }) | ||
215 | |||
216 | describe('Cut task', function () { | ||
217 | |||
218 | async function cut (start: number, end: number, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) { | ||
219 | await command.createEditionTasks({ | ||
220 | videoId: videoUUID, | ||
221 | tasks: [ | ||
222 | { | ||
223 | name: 'cut', | ||
224 | options: { | ||
225 | start, | ||
226 | end | ||
227 | } | ||
228 | } | ||
229 | ], | ||
230 | expectedStatus | ||
231 | }) | ||
232 | } | ||
233 | |||
234 | it('Should fail with bad start/end', async function () { | ||
235 | const invalid = [ | ||
236 | 'tintin', | ||
237 | -1, | ||
238 | undefined | ||
239 | ] | ||
240 | |||
241 | for (const value of invalid) { | ||
242 | await cut(value as any, undefined) | ||
243 | await cut(undefined, value as any) | ||
244 | } | ||
245 | }) | ||
246 | |||
247 | it('Should fail with the same start/end', async function () { | ||
248 | await cut(2, 2) | ||
249 | }) | ||
250 | |||
251 | it('Should fail with inconsistents start/end', async function () { | ||
252 | await cut(2, 1) | ||
253 | }) | ||
254 | |||
255 | it('Should fail without start and end', async function () { | ||
256 | await cut(undefined, undefined) | ||
257 | }) | ||
258 | |||
259 | it('Should succeed with the correct params', async function () { | ||
260 | this.timeout(120000) | ||
261 | |||
262 | await cut(0, 2, HttpStatusCode.NO_CONTENT_204) | ||
263 | |||
264 | await waitJobs([ server ]) | ||
265 | }) | ||
266 | }) | ||
267 | |||
268 | describe('Watermark task', function () { | ||
269 | |||
270 | async function addWatermark (file: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) { | ||
271 | await command.createEditionTasks({ | ||
272 | videoId: videoUUID, | ||
273 | tasks: [ | ||
274 | { | ||
275 | name: 'add-watermark', | ||
276 | options: { | ||
277 | file | ||
278 | } | ||
279 | } | ||
280 | ], | ||
281 | expectedStatus | ||
282 | }) | ||
283 | } | ||
284 | |||
285 | it('Should fail without waterkmark', async function () { | ||
286 | await addWatermark(undefined) | ||
287 | }) | ||
288 | |||
289 | it('Should fail with an invalid watermark', async function () { | ||
290 | await addWatermark('video_short.mp4') | ||
291 | }) | ||
292 | |||
293 | it('Should succeed with the correct params', async function () { | ||
294 | this.timeout(120000) | ||
295 | |||
296 | await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204) | ||
297 | |||
298 | await waitJobs([ server ]) | ||
299 | }) | ||
300 | }) | ||
301 | |||
302 | describe('Intro/Outro task', function () { | ||
303 | |||
304 | async function addIntroOutro ( | ||
305 | type: 'add-intro' | 'add-outro', | ||
306 | file: string, | ||
307 | expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400 | ||
308 | ) { | ||
309 | await command.createEditionTasks({ | ||
310 | videoId: videoUUID, | ||
311 | tasks: [ | ||
312 | { | ||
313 | name: type, | ||
314 | options: { | ||
315 | file | ||
316 | } | ||
317 | } | ||
318 | ], | ||
319 | expectedStatus | ||
320 | }) | ||
321 | } | ||
322 | |||
323 | it('Should fail without file', async function () { | ||
324 | await addIntroOutro('add-intro', undefined) | ||
325 | await addIntroOutro('add-outro', undefined) | ||
326 | }) | ||
327 | |||
328 | it('Should fail with an invalid file', async function () { | ||
329 | await addIntroOutro('add-intro', 'custom-thumbnail.jpg') | ||
330 | await addIntroOutro('add-outro', 'custom-thumbnail.jpg') | ||
331 | }) | ||
332 | |||
333 | it('Should fail with a file that does not contain video stream', async function () { | ||
334 | await addIntroOutro('add-intro', 'sample.ogg') | ||
335 | await addIntroOutro('add-outro', 'sample.ogg') | ||
336 | |||
337 | }) | ||
338 | |||
339 | it('Should succeed with the correct params', async function () { | ||
340 | this.timeout(120000) | ||
341 | |||
342 | await addIntroOutro('add-intro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204) | ||
343 | await waitJobs([ server ]) | ||
344 | |||
345 | await addIntroOutro('add-outro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204) | ||
346 | await waitJobs([ server ]) | ||
347 | }) | ||
348 | |||
349 | it('Should check total quota when creating the task', async function () { | ||
350 | this.timeout(120000) | ||
351 | |||
352 | const user = await server.users.create({ username: 'user_quota_1' }) | ||
353 | const token = await server.login.getAccessToken('user_quota_1') | ||
354 | const { uuid } = await server.videos.quickUpload({ token, name: 'video_quota_1', fixture: 'video_short.mp4' }) | ||
355 | |||
356 | const addIntroOutroByUser = (type: 'add-intro' | 'add-outro', expectedStatus: HttpStatusCodeType) => { | ||
357 | return command.createEditionTasks({ | ||
358 | token, | ||
359 | videoId: uuid, | ||
360 | tasks: [ | ||
361 | { | ||
362 | name: type, | ||
363 | options: { | ||
364 | file: 'video_short.mp4' | ||
365 | } | ||
366 | } | ||
367 | ], | ||
368 | expectedStatus | ||
369 | }) | ||
370 | } | ||
371 | |||
372 | await waitJobs([ server ]) | ||
373 | |||
374 | const { videoQuotaUsed } = await server.users.getMyQuotaUsed({ token }) | ||
375 | await server.users.update({ userId: user.id, videoQuota: Math.round(videoQuotaUsed * 2.5) }) | ||
376 | |||
377 | // Still valid | ||
378 | await addIntroOutroByUser('add-intro', HttpStatusCode.NO_CONTENT_204) | ||
379 | |||
380 | await waitJobs([ server ]) | ||
381 | |||
382 | // Too much quota | ||
383 | await addIntroOutroByUser('add-intro', HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
384 | await addIntroOutroByUser('add-outro', HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
385 | }) | ||
386 | }) | ||
387 | }) | ||
388 | |||
389 | after(async function () { | ||
390 | await cleanupTests([ server ]) | ||
391 | }) | ||
392 | }) | ||
diff --git a/packages/tests/src/api/check-params/video-token.ts b/packages/tests/src/api/check-params/video-token.ts new file mode 100644 index 000000000..5f838102d --- /dev/null +++ b/packages/tests/src/api/check-params/video-token.ts | |||
@@ -0,0 +1,70 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
5 | |||
6 | describe('Test video tokens', function () { | ||
7 | let server: PeerTubeServer | ||
8 | let privateVideoId: string | ||
9 | let passwordProtectedVideoId: string | ||
10 | let userToken: string | ||
11 | |||
12 | const videoPassword = 'password' | ||
13 | |||
14 | // --------------------------------------------------------------- | ||
15 | |||
16 | before(async function () { | ||
17 | this.timeout(300_000) | ||
18 | |||
19 | server = await createSingleServer(1) | ||
20 | await setAccessTokensToServers([ server ]) | ||
21 | { | ||
22 | const { uuid } = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE }) | ||
23 | privateVideoId = uuid | ||
24 | } | ||
25 | { | ||
26 | const { uuid } = await server.videos.quickUpload({ | ||
27 | name: 'password protected video', | ||
28 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
29 | videoPasswords: [ videoPassword ] | ||
30 | }) | ||
31 | passwordProtectedVideoId = uuid | ||
32 | } | ||
33 | userToken = await server.users.generateUserAndToken('user1') | ||
34 | }) | ||
35 | |||
36 | it('Should not generate tokens on private video for unauthenticated user', async function () { | ||
37 | await server.videoToken.create({ videoId: privateVideoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
38 | }) | ||
39 | |||
40 | it('Should not generate tokens of unknown video', async function () { | ||
41 | await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
42 | }) | ||
43 | |||
44 | it('Should not generate tokens with incorrect password', async function () { | ||
45 | await server.videoToken.create({ | ||
46 | videoId: passwordProtectedVideoId, | ||
47 | token: null, | ||
48 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
49 | videoPassword: 'incorrectPassword' | ||
50 | }) | ||
51 | }) | ||
52 | |||
53 | it('Should not generate tokens of a non owned video', async function () { | ||
54 | await server.videoToken.create({ videoId: privateVideoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
55 | }) | ||
56 | |||
57 | it('Should generate token', async function () { | ||
58 | await server.videoToken.create({ videoId: privateVideoId }) | ||
59 | }) | ||
60 | |||
61 | it('Should generate token on password protected video', async function () { | ||
62 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: null }) | ||
63 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: userToken }) | ||
64 | await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword }) | ||
65 | }) | ||
66 | |||
67 | after(async function () { | ||
68 | await cleanupTests([ server ]) | ||
69 | }) | ||
70 | }) | ||
diff --git a/packages/tests/src/api/check-params/videos-common-filters.ts b/packages/tests/src/api/check-params/videos-common-filters.ts new file mode 100644 index 000000000..dbae3010c --- /dev/null +++ b/packages/tests/src/api/check-params/videos-common-filters.ts | |||
@@ -0,0 +1,171 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { | ||
4 | HttpStatusCode, | ||
5 | HttpStatusCodeType, | ||
6 | UserRole, | ||
7 | VideoInclude, | ||
8 | VideoIncludeType, | ||
9 | VideoPrivacy, | ||
10 | VideoPrivacyType | ||
11 | } from '@peertube/peertube-models' | ||
12 | import { | ||
13 | cleanupTests, | ||
14 | createSingleServer, | ||
15 | makeGetRequest, | ||
16 | PeerTubeServer, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultVideoChannel | ||
19 | } from '@peertube/peertube-server-commands' | ||
20 | |||
21 | describe('Test video filters validators', function () { | ||
22 | let server: PeerTubeServer | ||
23 | let userAccessToken: string | ||
24 | let moderatorAccessToken: string | ||
25 | |||
26 | // --------------------------------------------------------------- | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(30000) | ||
30 | |||
31 | server = await createSingleServer(1) | ||
32 | |||
33 | await setAccessTokensToServers([ server ]) | ||
34 | await setDefaultVideoChannel([ server ]) | ||
35 | |||
36 | const user = { username: 'user1', password: 'my super password' } | ||
37 | await server.users.create({ username: user.username, password: user.password }) | ||
38 | userAccessToken = await server.login.getAccessToken(user) | ||
39 | |||
40 | const moderator = { username: 'moderator', password: 'my super password' } | ||
41 | await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR }) | ||
42 | |||
43 | moderatorAccessToken = await server.login.getAccessToken(moderator) | ||
44 | }) | ||
45 | |||
46 | describe('When setting video filters', function () { | ||
47 | |||
48 | const validIncludes = [ | ||
49 | VideoInclude.NONE, | ||
50 | VideoInclude.BLOCKED_OWNER, | ||
51 | VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED | ||
52 | ] | ||
53 | |||
54 | async function testEndpoints (options: { | ||
55 | token?: string | ||
56 | isLocal?: boolean | ||
57 | include?: VideoIncludeType | ||
58 | privacyOneOf?: VideoPrivacyType[] | ||
59 | expectedStatus: HttpStatusCodeType | ||
60 | excludeAlreadyWatched?: boolean | ||
61 | unauthenticatedUser?: boolean | ||
62 | }) { | ||
63 | const paths = [ | ||
64 | '/api/v1/video-channels/root_channel/videos', | ||
65 | '/api/v1/accounts/root/videos', | ||
66 | '/api/v1/videos', | ||
67 | '/api/v1/search/videos' | ||
68 | ] | ||
69 | |||
70 | for (const path of paths) { | ||
71 | const token = options.unauthenticatedUser | ||
72 | ? undefined | ||
73 | : options.token || server.accessToken | ||
74 | |||
75 | await makeGetRequest({ | ||
76 | url: server.url, | ||
77 | path, | ||
78 | token, | ||
79 | query: { | ||
80 | isLocal: options.isLocal, | ||
81 | privacyOneOf: options.privacyOneOf, | ||
82 | include: options.include, | ||
83 | excludeAlreadyWatched: options.excludeAlreadyWatched | ||
84 | }, | ||
85 | expectedStatus: options.expectedStatus | ||
86 | }) | ||
87 | } | ||
88 | } | ||
89 | |||
90 | it('Should fail with a bad privacyOneOf', async function () { | ||
91 | await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
92 | }) | ||
93 | |||
94 | it('Should succeed with a good privacyOneOf', async function () { | ||
95 | await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 }) | ||
96 | }) | ||
97 | |||
98 | it('Should fail to use privacyOneOf with a simple user', async function () { | ||
99 | await testEndpoints({ | ||
100 | privacyOneOf: [ VideoPrivacy.INTERNAL ], | ||
101 | token: userAccessToken, | ||
102 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
103 | }) | ||
104 | }) | ||
105 | |||
106 | it('Should fail with a bad include', async function () { | ||
107 | await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
108 | }) | ||
109 | |||
110 | it('Should succeed with a good include', async function () { | ||
111 | for (const include of validIncludes) { | ||
112 | await testEndpoints({ include, expectedStatus: HttpStatusCode.OK_200 }) | ||
113 | } | ||
114 | }) | ||
115 | |||
116 | it('Should fail to include more videos with a simple user', async function () { | ||
117 | for (const include of validIncludes) { | ||
118 | await testEndpoints({ token: userAccessToken, include, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
119 | } | ||
120 | }) | ||
121 | |||
122 | it('Should succeed to list all local/all with a moderator', async function () { | ||
123 | for (const include of validIncludes) { | ||
124 | await testEndpoints({ token: moderatorAccessToken, include, expectedStatus: HttpStatusCode.OK_200 }) | ||
125 | } | ||
126 | }) | ||
127 | |||
128 | it('Should succeed to list all local/all with an admin', async function () { | ||
129 | for (const include of validIncludes) { | ||
130 | await testEndpoints({ token: server.accessToken, include, expectedStatus: HttpStatusCode.OK_200 }) | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | // Because we cannot authenticate the user on the RSS endpoint | ||
135 | it('Should fail on the feeds endpoint with the all filter', async function () { | ||
136 | for (const include of [ VideoInclude.NOT_PUBLISHED_STATE ]) { | ||
137 | await makeGetRequest({ | ||
138 | url: server.url, | ||
139 | path: '/feeds/videos.json', | ||
140 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401, | ||
141 | query: { | ||
142 | include | ||
143 | } | ||
144 | }) | ||
145 | } | ||
146 | }) | ||
147 | |||
148 | it('Should succeed on the feeds endpoint with the local filter', async function () { | ||
149 | await makeGetRequest({ | ||
150 | url: server.url, | ||
151 | path: '/feeds/videos.json', | ||
152 | expectedStatus: HttpStatusCode.OK_200, | ||
153 | query: { | ||
154 | isLocal: true | ||
155 | } | ||
156 | }) | ||
157 | }) | ||
158 | |||
159 | it('Should fail when trying to exclude already watched videos for an unlogged user', async function () { | ||
160 | await testEndpoints({ excludeAlreadyWatched: true, unauthenticatedUser: true, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
161 | }) | ||
162 | |||
163 | it('Should succeed when trying to exclude already watched videos for a logged user', async function () { | ||
164 | await testEndpoints({ token: userAccessToken, excludeAlreadyWatched: true, expectedStatus: HttpStatusCode.OK_200 }) | ||
165 | }) | ||
166 | }) | ||
167 | |||
168 | after(async function () { | ||
169 | await cleanupTests([ server ]) | ||
170 | }) | ||
171 | }) | ||
diff --git a/packages/tests/src/api/check-params/videos-history.ts b/packages/tests/src/api/check-params/videos-history.ts new file mode 100644 index 000000000..65d1e9fac --- /dev/null +++ b/packages/tests/src/api/check-params/videos-history.ts | |||
@@ -0,0 +1,145 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { checkBadCountPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeDeleteRequest, | ||
9 | makeGetRequest, | ||
10 | makePostBodyRequest, | ||
11 | makePutBodyRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Test videos history API validator', function () { | ||
17 | const myHistoryPath = '/api/v1/users/me/history/videos' | ||
18 | const myHistoryRemove = myHistoryPath + '/remove' | ||
19 | let viewPath: string | ||
20 | let server: PeerTubeServer | ||
21 | let videoId: number | ||
22 | |||
23 | // --------------------------------------------------------------- | ||
24 | |||
25 | before(async function () { | ||
26 | this.timeout(30000) | ||
27 | |||
28 | server = await createSingleServer(1) | ||
29 | |||
30 | await setAccessTokensToServers([ server ]) | ||
31 | |||
32 | const { id, uuid } = await server.videos.upload() | ||
33 | viewPath = '/api/v1/videos/' + uuid + '/views' | ||
34 | videoId = id | ||
35 | }) | ||
36 | |||
37 | describe('When notifying a user is watching a video', function () { | ||
38 | |||
39 | it('Should fail with a bad token', async function () { | ||
40 | const fields = { currentTime: 5 } | ||
41 | await makePutBodyRequest({ url: server.url, path: viewPath, fields, token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
42 | }) | ||
43 | |||
44 | it('Should succeed with the correct parameters', async function () { | ||
45 | const fields = { currentTime: 5 } | ||
46 | |||
47 | await makePutBodyRequest({ | ||
48 | url: server.url, | ||
49 | path: viewPath, | ||
50 | fields, | ||
51 | token: server.accessToken, | ||
52 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
53 | }) | ||
54 | }) | ||
55 | }) | ||
56 | |||
57 | describe('When listing user videos history', function () { | ||
58 | it('Should fail with a bad start pagination', async function () { | ||
59 | await checkBadStartPagination(server.url, myHistoryPath, server.accessToken) | ||
60 | }) | ||
61 | |||
62 | it('Should fail with a bad count pagination', async function () { | ||
63 | await checkBadCountPagination(server.url, myHistoryPath, server.accessToken) | ||
64 | }) | ||
65 | |||
66 | it('Should fail with an unauthenticated user', async function () { | ||
67 | await makeGetRequest({ url: server.url, path: myHistoryPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
68 | }) | ||
69 | |||
70 | it('Should succeed with the correct params', async function () { | ||
71 | await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
72 | }) | ||
73 | }) | ||
74 | |||
75 | describe('When removing a specific user video history element', function () { | ||
76 | let path: string | ||
77 | |||
78 | before(function () { | ||
79 | path = myHistoryPath + '/' + videoId | ||
80 | }) | ||
81 | |||
82 | it('Should fail with an unauthenticated user', async function () { | ||
83 | await makeDeleteRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
84 | }) | ||
85 | |||
86 | it('Should fail with a bad videoId parameter', async function () { | ||
87 | await makeDeleteRequest({ | ||
88 | url: server.url, | ||
89 | token: server.accessToken, | ||
90 | path: myHistoryRemove + '/hi', | ||
91 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
92 | }) | ||
93 | }) | ||
94 | |||
95 | it('Should succeed with the correct parameters', async function () { | ||
96 | await makeDeleteRequest({ | ||
97 | url: server.url, | ||
98 | token: server.accessToken, | ||
99 | path, | ||
100 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
101 | }) | ||
102 | }) | ||
103 | }) | ||
104 | |||
105 | describe('When removing all user videos history', function () { | ||
106 | it('Should fail with an unauthenticated user', async function () { | ||
107 | await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
108 | }) | ||
109 | |||
110 | it('Should fail with a bad beforeDate parameter', async function () { | ||
111 | const body = { beforeDate: '15' } | ||
112 | await makePostBodyRequest({ | ||
113 | url: server.url, | ||
114 | token: server.accessToken, | ||
115 | path: myHistoryRemove, | ||
116 | fields: body, | ||
117 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
118 | }) | ||
119 | }) | ||
120 | |||
121 | it('Should succeed with a valid beforeDate param', async function () { | ||
122 | const body = { beforeDate: new Date().toISOString() } | ||
123 | await makePostBodyRequest({ | ||
124 | url: server.url, | ||
125 | token: server.accessToken, | ||
126 | path: myHistoryRemove, | ||
127 | fields: body, | ||
128 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
129 | }) | ||
130 | }) | ||
131 | |||
132 | it('Should succeed without body', async function () { | ||
133 | await makePostBodyRequest({ | ||
134 | url: server.url, | ||
135 | token: server.accessToken, | ||
136 | path: myHistoryRemove, | ||
137 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
138 | }) | ||
139 | }) | ||
140 | }) | ||
141 | |||
142 | after(async function () { | ||
143 | await cleanupTests([ server ]) | ||
144 | }) | ||
145 | }) | ||
diff --git a/packages/tests/src/api/check-params/videos-overviews.ts b/packages/tests/src/api/check-params/videos-overviews.ts new file mode 100644 index 000000000..ba6f6ac69 --- /dev/null +++ b/packages/tests/src/api/check-params/videos-overviews.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
4 | |||
5 | describe('Test videos overview API validator', function () { | ||
6 | let server: PeerTubeServer | ||
7 | |||
8 | // --------------------------------------------------------------- | ||
9 | |||
10 | before(async function () { | ||
11 | this.timeout(30000) | ||
12 | |||
13 | server = await createSingleServer(1) | ||
14 | }) | ||
15 | |||
16 | describe('When getting videos overview', function () { | ||
17 | |||
18 | it('Should fail with a bad pagination', async function () { | ||
19 | await server.overviews.getVideos({ page: 0, expectedStatus: 400 }) | ||
20 | await server.overviews.getVideos({ page: 100, expectedStatus: 400 }) | ||
21 | }) | ||
22 | |||
23 | it('Should succeed with a good pagination', async function () { | ||
24 | await server.overviews.getVideos({ page: 1 }) | ||
25 | }) | ||
26 | }) | ||
27 | |||
28 | after(async function () { | ||
29 | await cleanupTests([ server ]) | ||
30 | }) | ||
31 | }) | ||
diff --git a/packages/tests/src/api/check-params/videos.ts b/packages/tests/src/api/check-params/videos.ts new file mode 100644 index 000000000..c349ed9fe --- /dev/null +++ b/packages/tests/src/api/check-params/videos.ts | |||
@@ -0,0 +1,883 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { join } from 'path' | ||
5 | import { omit, randomInt } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, PeerTubeProblemDocument, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | makeDeleteRequest, | ||
12 | makeGetRequest, | ||
13 | makePutBodyRequest, | ||
14 | makeUploadRequest, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js' | ||
19 | import { checkUploadVideoParam } from '@tests/shared/videos.js' | ||
20 | |||
21 | describe('Test videos API validator', function () { | ||
22 | const path = '/api/v1/videos/' | ||
23 | let server: PeerTubeServer | ||
24 | let userAccessToken = '' | ||
25 | let accountName: string | ||
26 | let channelId: number | ||
27 | let channelName: string | ||
28 | let video: VideoCreateResult | ||
29 | let privateVideo: VideoCreateResult | ||
30 | |||
31 | // --------------------------------------------------------------- | ||
32 | |||
33 | before(async function () { | ||
34 | this.timeout(30000) | ||
35 | |||
36 | server = await createSingleServer(1) | ||
37 | |||
38 | await setAccessTokensToServers([ server ]) | ||
39 | |||
40 | userAccessToken = await server.users.generateUserAndToken('user1') | ||
41 | |||
42 | { | ||
43 | const body = await server.users.getMyInfo() | ||
44 | channelId = body.videoChannels[0].id | ||
45 | channelName = body.videoChannels[0].name | ||
46 | accountName = body.account.name + '@' + body.account.host | ||
47 | } | ||
48 | |||
49 | { | ||
50 | privateVideo = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE }) | ||
51 | } | ||
52 | }) | ||
53 | |||
54 | describe('When listing videos', function () { | ||
55 | it('Should fail with a bad start pagination', async function () { | ||
56 | await checkBadStartPagination(server.url, path) | ||
57 | }) | ||
58 | |||
59 | it('Should fail with a bad count pagination', async function () { | ||
60 | await checkBadCountPagination(server.url, path) | ||
61 | }) | ||
62 | |||
63 | it('Should fail with an incorrect sort', async function () { | ||
64 | await checkBadSortPagination(server.url, path) | ||
65 | }) | ||
66 | |||
67 | it('Should fail with a bad skipVideos query', async function () { | ||
68 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: 'toto' } }) | ||
69 | }) | ||
70 | |||
71 | it('Should success with the correct parameters', async function () { | ||
72 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: false } }) | ||
73 | }) | ||
74 | }) | ||
75 | |||
76 | describe('When searching a video', function () { | ||
77 | |||
78 | it('Should fail with nothing', async function () { | ||
79 | await makeGetRequest({ | ||
80 | url: server.url, | ||
81 | path: join(path, 'search'), | ||
82 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
83 | }) | ||
84 | }) | ||
85 | |||
86 | it('Should fail with a bad start pagination', async function () { | ||
87 | await checkBadStartPagination(server.url, join(path, 'search', 'test')) | ||
88 | }) | ||
89 | |||
90 | it('Should fail with a bad count pagination', async function () { | ||
91 | await checkBadCountPagination(server.url, join(path, 'search', 'test')) | ||
92 | }) | ||
93 | |||
94 | it('Should fail with an incorrect sort', async function () { | ||
95 | await checkBadSortPagination(server.url, join(path, 'search', 'test')) | ||
96 | }) | ||
97 | |||
98 | it('Should success with the correct parameters', async function () { | ||
99 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 }) | ||
100 | }) | ||
101 | }) | ||
102 | |||
103 | describe('When listing my videos', function () { | ||
104 | const path = '/api/v1/users/me/videos' | ||
105 | |||
106 | it('Should fail with a bad start pagination', async function () { | ||
107 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
108 | }) | ||
109 | |||
110 | it('Should fail with a bad count pagination', async function () { | ||
111 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
112 | }) | ||
113 | |||
114 | it('Should fail with an incorrect sort', async function () { | ||
115 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
116 | }) | ||
117 | |||
118 | it('Should fail with an invalid channel', async function () { | ||
119 | await makeGetRequest({ url: server.url, token: server.accessToken, path, query: { channelId: 'toto' } }) | ||
120 | }) | ||
121 | |||
122 | it('Should fail with an unknown channel', async function () { | ||
123 | await makeGetRequest({ | ||
124 | url: server.url, | ||
125 | token: server.accessToken, | ||
126 | path, | ||
127 | query: { channelId: 89898 }, | ||
128 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
129 | }) | ||
130 | }) | ||
131 | |||
132 | it('Should success with the correct parameters', async function () { | ||
133 | await makeGetRequest({ url: server.url, token: server.accessToken, path, expectedStatus: HttpStatusCode.OK_200 }) | ||
134 | }) | ||
135 | }) | ||
136 | |||
137 | describe('When listing account videos', function () { | ||
138 | let path: string | ||
139 | |||
140 | before(async function () { | ||
141 | path = '/api/v1/accounts/' + accountName + '/videos' | ||
142 | }) | ||
143 | |||
144 | it('Should fail with a bad start pagination', async function () { | ||
145 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
146 | }) | ||
147 | |||
148 | it('Should fail with a bad count pagination', async function () { | ||
149 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
150 | }) | ||
151 | |||
152 | it('Should fail with an incorrect sort', async function () { | ||
153 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
154 | }) | ||
155 | |||
156 | it('Should success with the correct parameters', async function () { | ||
157 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 }) | ||
158 | }) | ||
159 | }) | ||
160 | |||
161 | describe('When listing video channel videos', function () { | ||
162 | let path: string | ||
163 | |||
164 | before(async function () { | ||
165 | path = '/api/v1/video-channels/' + channelName + '/videos' | ||
166 | }) | ||
167 | |||
168 | it('Should fail with a bad start pagination', async function () { | ||
169 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
170 | }) | ||
171 | |||
172 | it('Should fail with a bad count pagination', async function () { | ||
173 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
174 | }) | ||
175 | |||
176 | it('Should fail with an incorrect sort', async function () { | ||
177 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
178 | }) | ||
179 | |||
180 | it('Should success with the correct parameters', async function () { | ||
181 | await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 }) | ||
182 | }) | ||
183 | }) | ||
184 | |||
185 | describe('When adding a video', function () { | ||
186 | let baseCorrectParams | ||
187 | const baseCorrectAttaches = { | ||
188 | fixture: buildAbsoluteFixturePath('video_short.webm') | ||
189 | } | ||
190 | |||
191 | before(function () { | ||
192 | // Put in before to have channelId | ||
193 | baseCorrectParams = { | ||
194 | name: 'my super name', | ||
195 | category: 5, | ||
196 | licence: 1, | ||
197 | language: 'pt', | ||
198 | nsfw: false, | ||
199 | commentsEnabled: true, | ||
200 | downloadEnabled: true, | ||
201 | waitTranscoding: true, | ||
202 | description: 'my super description', | ||
203 | support: 'my super support text', | ||
204 | tags: [ 'tag1', 'tag2' ], | ||
205 | privacy: VideoPrivacy.PUBLIC, | ||
206 | channelId, | ||
207 | originallyPublishedAt: new Date().toISOString() | ||
208 | } | ||
209 | }) | ||
210 | |||
211 | function runSuite (mode: 'legacy' | 'resumable') { | ||
212 | |||
213 | const baseOptions = () => { | ||
214 | return { | ||
215 | server, | ||
216 | token: server.accessToken, | ||
217 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | ||
218 | mode | ||
219 | } | ||
220 | } | ||
221 | |||
222 | it('Should fail with nothing', async function () { | ||
223 | const fields = {} | ||
224 | const attaches = {} | ||
225 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
226 | }) | ||
227 | |||
228 | it('Should fail without name', async function () { | ||
229 | const fields = omit(baseCorrectParams, [ 'name' ]) | ||
230 | const attaches = baseCorrectAttaches | ||
231 | |||
232 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
233 | }) | ||
234 | |||
235 | it('Should fail with a long name', async function () { | ||
236 | const fields = { ...baseCorrectParams, name: 'super'.repeat(65) } | ||
237 | const attaches = baseCorrectAttaches | ||
238 | |||
239 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
240 | }) | ||
241 | |||
242 | it('Should fail with a bad category', async function () { | ||
243 | const fields = { ...baseCorrectParams, category: 125 } | ||
244 | const attaches = baseCorrectAttaches | ||
245 | |||
246 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
247 | }) | ||
248 | |||
249 | it('Should fail with a bad licence', async function () { | ||
250 | const fields = { ...baseCorrectParams, licence: 125 } | ||
251 | const attaches = baseCorrectAttaches | ||
252 | |||
253 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
254 | }) | ||
255 | |||
256 | it('Should fail with a bad language', async function () { | ||
257 | const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } | ||
258 | const attaches = baseCorrectAttaches | ||
259 | |||
260 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
261 | }) | ||
262 | |||
263 | it('Should fail with a long description', async function () { | ||
264 | const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) } | ||
265 | const attaches = baseCorrectAttaches | ||
266 | |||
267 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
268 | }) | ||
269 | |||
270 | it('Should fail with a long support text', async function () { | ||
271 | const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } | ||
272 | const attaches = baseCorrectAttaches | ||
273 | |||
274 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
275 | }) | ||
276 | |||
277 | it('Should fail without a channel', async function () { | ||
278 | const fields = omit(baseCorrectParams, [ 'channelId' ]) | ||
279 | const attaches = baseCorrectAttaches | ||
280 | |||
281 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
282 | }) | ||
283 | |||
284 | it('Should fail with a bad channel', async function () { | ||
285 | const fields = { ...baseCorrectParams, channelId: 545454 } | ||
286 | const attaches = baseCorrectAttaches | ||
287 | |||
288 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
289 | }) | ||
290 | |||
291 | it('Should fail with another user channel', async function () { | ||
292 | const user = { | ||
293 | username: 'fake' + randomInt(0, 1500), | ||
294 | password: 'fake_password' | ||
295 | } | ||
296 | await server.users.create({ username: user.username, password: user.password }) | ||
297 | |||
298 | const accessTokenUser = await server.login.getAccessToken(user) | ||
299 | const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser }) | ||
300 | const customChannelId = videoChannels[0].id | ||
301 | |||
302 | const fields = { ...baseCorrectParams, channelId: customChannelId } | ||
303 | const attaches = baseCorrectAttaches | ||
304 | |||
305 | await checkUploadVideoParam({ | ||
306 | ...baseOptions(), | ||
307 | token: userAccessToken, | ||
308 | attributes: { ...fields, ...attaches } | ||
309 | }) | ||
310 | }) | ||
311 | |||
312 | it('Should fail with too many tags', async function () { | ||
313 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] } | ||
314 | const attaches = baseCorrectAttaches | ||
315 | |||
316 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
317 | }) | ||
318 | |||
319 | it('Should fail with a tag length too low', async function () { | ||
320 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] } | ||
321 | const attaches = baseCorrectAttaches | ||
322 | |||
323 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
324 | }) | ||
325 | |||
326 | it('Should fail with a tag length too big', async function () { | ||
327 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] } | ||
328 | const attaches = baseCorrectAttaches | ||
329 | |||
330 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
331 | }) | ||
332 | |||
333 | it('Should fail with a bad schedule update (miss updateAt)', async function () { | ||
334 | const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } } | ||
335 | const attaches = baseCorrectAttaches | ||
336 | |||
337 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
338 | }) | ||
339 | |||
340 | it('Should fail with a bad schedule update (wrong updateAt)', async function () { | ||
341 | const fields = { | ||
342 | ...baseCorrectParams, | ||
343 | |||
344 | scheduleUpdate: { | ||
345 | privacy: VideoPrivacy.PUBLIC, | ||
346 | updateAt: 'toto' | ||
347 | } | ||
348 | } | ||
349 | const attaches = baseCorrectAttaches | ||
350 | |||
351 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
352 | }) | ||
353 | |||
354 | it('Should fail with a bad originally published at attribute', async function () { | ||
355 | const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' } | ||
356 | const attaches = baseCorrectAttaches | ||
357 | |||
358 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
359 | }) | ||
360 | |||
361 | it('Should fail without an input file', async function () { | ||
362 | const fields = baseCorrectParams | ||
363 | const attaches = {} | ||
364 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
365 | }) | ||
366 | |||
367 | it('Should fail with an incorrect input file', async function () { | ||
368 | const fields = baseCorrectParams | ||
369 | let attaches = { fixture: buildAbsoluteFixturePath('video_short_fake.webm') } | ||
370 | |||
371 | await checkUploadVideoParam({ | ||
372 | ...baseOptions(), | ||
373 | attributes: { ...fields, ...attaches }, | ||
374 | // 200 for the init request, 422 when the file has finished being uploaded | ||
375 | expectedStatus: undefined, | ||
376 | completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 | ||
377 | }) | ||
378 | |||
379 | attaches = { fixture: buildAbsoluteFixturePath('video_short.mkv') } | ||
380 | await checkUploadVideoParam({ | ||
381 | ...baseOptions(), | ||
382 | attributes: { ...fields, ...attaches }, | ||
383 | expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 | ||
384 | }) | ||
385 | }) | ||
386 | |||
387 | it('Should fail with an incorrect thumbnail file', async function () { | ||
388 | const fields = baseCorrectParams | ||
389 | const attaches = { | ||
390 | thumbnailfile: buildAbsoluteFixturePath('video_short.mp4'), | ||
391 | fixture: buildAbsoluteFixturePath('video_short.mp4') | ||
392 | } | ||
393 | |||
394 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
395 | }) | ||
396 | |||
397 | it('Should fail with a big thumbnail file', async function () { | ||
398 | const fields = baseCorrectParams | ||
399 | const attaches = { | ||
400 | thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png'), | ||
401 | fixture: buildAbsoluteFixturePath('video_short.mp4') | ||
402 | } | ||
403 | |||
404 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
405 | }) | ||
406 | |||
407 | it('Should fail with an incorrect preview file', async function () { | ||
408 | const fields = baseCorrectParams | ||
409 | const attaches = { | ||
410 | previewfile: buildAbsoluteFixturePath('video_short.mp4'), | ||
411 | fixture: buildAbsoluteFixturePath('video_short.mp4') | ||
412 | } | ||
413 | |||
414 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
415 | }) | ||
416 | |||
417 | it('Should fail with a big preview file', async function () { | ||
418 | const fields = baseCorrectParams | ||
419 | const attaches = { | ||
420 | previewfile: buildAbsoluteFixturePath('custom-preview-big.png'), | ||
421 | fixture: buildAbsoluteFixturePath('video_short.mp4') | ||
422 | } | ||
423 | |||
424 | await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) | ||
425 | }) | ||
426 | |||
427 | it('Should report the appropriate error', async function () { | ||
428 | const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } | ||
429 | const attaches = baseCorrectAttaches | ||
430 | |||
431 | const attributes = { ...fields, ...attaches } | ||
432 | const body = await checkUploadVideoParam({ ...baseOptions(), attributes }) | ||
433 | |||
434 | const error = body as unknown as PeerTubeProblemDocument | ||
435 | |||
436 | if (mode === 'legacy') { | ||
437 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy') | ||
438 | } else { | ||
439 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit') | ||
440 | } | ||
441 | |||
442 | expect(error.type).to.equal('about:blank') | ||
443 | expect(error.title).to.equal('Bad Request') | ||
444 | |||
445 | expect(error.detail).to.equal('Incorrect request parameters: language') | ||
446 | expect(error.error).to.equal('Incorrect request parameters: language') | ||
447 | |||
448 | expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) | ||
449 | expect(error['invalid-params'].language).to.exist | ||
450 | }) | ||
451 | |||
452 | it('Should succeed with the correct parameters', async function () { | ||
453 | this.timeout(30000) | ||
454 | |||
455 | const fields = baseCorrectParams | ||
456 | |||
457 | { | ||
458 | const attaches = baseCorrectAttaches | ||
459 | await checkUploadVideoParam({ | ||
460 | ...baseOptions(), | ||
461 | attributes: { ...fields, ...attaches }, | ||
462 | expectedStatus: HttpStatusCode.OK_200 | ||
463 | }) | ||
464 | } | ||
465 | |||
466 | { | ||
467 | const attaches = { | ||
468 | ...baseCorrectAttaches, | ||
469 | |||
470 | videofile: buildAbsoluteFixturePath('video_short.mp4') | ||
471 | } | ||
472 | |||
473 | await checkUploadVideoParam({ | ||
474 | ...baseOptions(), | ||
475 | attributes: { ...fields, ...attaches }, | ||
476 | expectedStatus: HttpStatusCode.OK_200 | ||
477 | }) | ||
478 | } | ||
479 | |||
480 | { | ||
481 | const attaches = { | ||
482 | ...baseCorrectAttaches, | ||
483 | |||
484 | videofile: buildAbsoluteFixturePath('video_short.ogv') | ||
485 | } | ||
486 | |||
487 | await checkUploadVideoParam({ | ||
488 | ...baseOptions(), | ||
489 | attributes: { ...fields, ...attaches }, | ||
490 | expectedStatus: HttpStatusCode.OK_200 | ||
491 | }) | ||
492 | } | ||
493 | }) | ||
494 | } | ||
495 | |||
496 | describe('Resumable upload', function () { | ||
497 | runSuite('resumable') | ||
498 | }) | ||
499 | |||
500 | describe('Legacy upload', function () { | ||
501 | runSuite('legacy') | ||
502 | }) | ||
503 | }) | ||
504 | |||
505 | describe('When updating a video', function () { | ||
506 | const baseCorrectParams = { | ||
507 | name: 'my super name', | ||
508 | category: 5, | ||
509 | licence: 2, | ||
510 | language: 'pt', | ||
511 | nsfw: false, | ||
512 | commentsEnabled: false, | ||
513 | downloadEnabled: false, | ||
514 | description: 'my super description', | ||
515 | privacy: VideoPrivacy.PUBLIC, | ||
516 | tags: [ 'tag1', 'tag2' ] | ||
517 | } | ||
518 | |||
519 | before(async function () { | ||
520 | const { data } = await server.videos.list() | ||
521 | video = data[0] | ||
522 | }) | ||
523 | |||
524 | it('Should fail with nothing', async function () { | ||
525 | const fields = {} | ||
526 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
527 | }) | ||
528 | |||
529 | it('Should fail without a valid uuid', async function () { | ||
530 | const fields = baseCorrectParams | ||
531 | await makePutBodyRequest({ url: server.url, path: path + 'blabla', token: server.accessToken, fields }) | ||
532 | }) | ||
533 | |||
534 | it('Should fail with an unknown id', async function () { | ||
535 | const fields = baseCorrectParams | ||
536 | |||
537 | await makePutBodyRequest({ | ||
538 | url: server.url, | ||
539 | path: path + '4da6fde3-88f7-4d16-b119-108df5630b06', | ||
540 | token: server.accessToken, | ||
541 | fields, | ||
542 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
543 | }) | ||
544 | }) | ||
545 | |||
546 | it('Should fail with a long name', async function () { | ||
547 | const fields = { ...baseCorrectParams, name: 'super'.repeat(65) } | ||
548 | |||
549 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
550 | }) | ||
551 | |||
552 | it('Should fail with a bad category', async function () { | ||
553 | const fields = { ...baseCorrectParams, category: 125 } | ||
554 | |||
555 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
556 | }) | ||
557 | |||
558 | it('Should fail with a bad licence', async function () { | ||
559 | const fields = { ...baseCorrectParams, licence: 125 } | ||
560 | |||
561 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
562 | }) | ||
563 | |||
564 | it('Should fail with a bad language', async function () { | ||
565 | const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } | ||
566 | |||
567 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
568 | }) | ||
569 | |||
570 | it('Should fail with a long description', async function () { | ||
571 | const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) } | ||
572 | |||
573 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
574 | }) | ||
575 | |||
576 | it('Should fail with a long support text', async function () { | ||
577 | const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } | ||
578 | |||
579 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
580 | }) | ||
581 | |||
582 | it('Should fail with a bad channel', async function () { | ||
583 | const fields = { ...baseCorrectParams, channelId: 545454 } | ||
584 | |||
585 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
586 | }) | ||
587 | |||
588 | it('Should fail with too many tags', async function () { | ||
589 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] } | ||
590 | |||
591 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
592 | }) | ||
593 | |||
594 | it('Should fail with a tag length too low', async function () { | ||
595 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] } | ||
596 | |||
597 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
598 | }) | ||
599 | |||
600 | it('Should fail with a tag length too big', async function () { | ||
601 | const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] } | ||
602 | |||
603 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
604 | }) | ||
605 | |||
606 | it('Should fail with a bad schedule update (miss updateAt)', async function () { | ||
607 | const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } } | ||
608 | |||
609 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
610 | }) | ||
611 | |||
612 | it('Should fail with a bad schedule update (wrong updateAt)', async function () { | ||
613 | const fields = { ...baseCorrectParams, scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } } | ||
614 | |||
615 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
616 | }) | ||
617 | |||
618 | it('Should fail with a bad originally published at param', async function () { | ||
619 | const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' } | ||
620 | |||
621 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
622 | }) | ||
623 | |||
624 | it('Should fail with an incorrect thumbnail file', async function () { | ||
625 | const fields = baseCorrectParams | ||
626 | const attaches = { | ||
627 | thumbnailfile: buildAbsoluteFixturePath('video_short.mp4') | ||
628 | } | ||
629 | |||
630 | await makeUploadRequest({ | ||
631 | url: server.url, | ||
632 | method: 'PUT', | ||
633 | path: path + video.shortUUID, | ||
634 | token: server.accessToken, | ||
635 | fields, | ||
636 | attaches | ||
637 | }) | ||
638 | }) | ||
639 | |||
640 | it('Should fail with a big thumbnail file', async function () { | ||
641 | const fields = baseCorrectParams | ||
642 | const attaches = { | ||
643 | thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') | ||
644 | } | ||
645 | |||
646 | await makeUploadRequest({ | ||
647 | url: server.url, | ||
648 | method: 'PUT', | ||
649 | path: path + video.shortUUID, | ||
650 | token: server.accessToken, | ||
651 | fields, | ||
652 | attaches | ||
653 | }) | ||
654 | }) | ||
655 | |||
656 | it('Should fail with an incorrect preview file', async function () { | ||
657 | const fields = baseCorrectParams | ||
658 | const attaches = { | ||
659 | previewfile: buildAbsoluteFixturePath('video_short.mp4') | ||
660 | } | ||
661 | |||
662 | await makeUploadRequest({ | ||
663 | url: server.url, | ||
664 | method: 'PUT', | ||
665 | path: path + video.shortUUID, | ||
666 | token: server.accessToken, | ||
667 | fields, | ||
668 | attaches | ||
669 | }) | ||
670 | }) | ||
671 | |||
672 | it('Should fail with a big preview file', async function () { | ||
673 | const fields = baseCorrectParams | ||
674 | const attaches = { | ||
675 | previewfile: buildAbsoluteFixturePath('custom-preview-big.png') | ||
676 | } | ||
677 | |||
678 | await makeUploadRequest({ | ||
679 | url: server.url, | ||
680 | method: 'PUT', | ||
681 | path: path + video.shortUUID, | ||
682 | token: server.accessToken, | ||
683 | fields, | ||
684 | attaches | ||
685 | }) | ||
686 | }) | ||
687 | |||
688 | it('Should fail with a video of another user without the appropriate right', async function () { | ||
689 | const fields = baseCorrectParams | ||
690 | |||
691 | await makePutBodyRequest({ | ||
692 | url: server.url, | ||
693 | path: path + video.shortUUID, | ||
694 | token: userAccessToken, | ||
695 | fields, | ||
696 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
697 | }) | ||
698 | }) | ||
699 | |||
700 | it('Should fail with a video of another server') | ||
701 | |||
702 | it('Shoud report the appropriate error', async function () { | ||
703 | const fields = { ...baseCorrectParams, licence: 125 } | ||
704 | |||
705 | const res = await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
706 | const error = res.body as PeerTubeProblemDocument | ||
707 | |||
708 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo') | ||
709 | |||
710 | expect(error.type).to.equal('about:blank') | ||
711 | expect(error.title).to.equal('Bad Request') | ||
712 | |||
713 | expect(error.detail).to.equal('Incorrect request parameters: licence') | ||
714 | expect(error.error).to.equal('Incorrect request parameters: licence') | ||
715 | |||
716 | expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) | ||
717 | expect(error['invalid-params'].licence).to.exist | ||
718 | }) | ||
719 | |||
720 | it('Should succeed with the correct parameters', async function () { | ||
721 | const fields = baseCorrectParams | ||
722 | |||
723 | await makePutBodyRequest({ | ||
724 | url: server.url, | ||
725 | path: path + video.shortUUID, | ||
726 | token: server.accessToken, | ||
727 | fields, | ||
728 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
729 | }) | ||
730 | }) | ||
731 | }) | ||
732 | |||
733 | describe('When getting a video', function () { | ||
734 | it('Should return the list of the videos with nothing', async function () { | ||
735 | const res = await makeGetRequest({ | ||
736 | url: server.url, | ||
737 | path, | ||
738 | expectedStatus: HttpStatusCode.OK_200 | ||
739 | }) | ||
740 | |||
741 | expect(res.body.data).to.be.an('array') | ||
742 | expect(res.body.data.length).to.equal(6) | ||
743 | }) | ||
744 | |||
745 | it('Should fail without a correct uuid', async function () { | ||
746 | await server.videos.get({ id: 'coucou', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
747 | }) | ||
748 | |||
749 | it('Should return 404 with an incorrect video', async function () { | ||
750 | await server.videos.get({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
751 | }) | ||
752 | |||
753 | it('Shoud report the appropriate error', async function () { | ||
754 | const body = await server.videos.get({ id: 'hi', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
755 | const error = body as unknown as PeerTubeProblemDocument | ||
756 | |||
757 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo') | ||
758 | |||
759 | expect(error.type).to.equal('about:blank') | ||
760 | expect(error.title).to.equal('Bad Request') | ||
761 | |||
762 | expect(error.detail).to.equal('Incorrect request parameters: id') | ||
763 | expect(error.error).to.equal('Incorrect request parameters: id') | ||
764 | |||
765 | expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) | ||
766 | expect(error['invalid-params'].id).to.exist | ||
767 | }) | ||
768 | |||
769 | it('Should succeed with the correct parameters', async function () { | ||
770 | await server.videos.get({ id: video.shortUUID }) | ||
771 | }) | ||
772 | }) | ||
773 | |||
774 | describe('When rating a video', function () { | ||
775 | let videoId: number | ||
776 | |||
777 | before(async function () { | ||
778 | const { data } = await server.videos.list() | ||
779 | videoId = data[0].id | ||
780 | }) | ||
781 | |||
782 | it('Should fail without a valid uuid', async function () { | ||
783 | const fields = { | ||
784 | rating: 'like' | ||
785 | } | ||
786 | await makePutBodyRequest({ url: server.url, path: path + 'blabla/rate', token: server.accessToken, fields }) | ||
787 | }) | ||
788 | |||
789 | it('Should fail with an unknown id', async function () { | ||
790 | const fields = { | ||
791 | rating: 'like' | ||
792 | } | ||
793 | await makePutBodyRequest({ | ||
794 | url: server.url, | ||
795 | path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate', | ||
796 | token: server.accessToken, | ||
797 | fields, | ||
798 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
799 | }) | ||
800 | }) | ||
801 | |||
802 | it('Should fail with a wrong rating', async function () { | ||
803 | const fields = { | ||
804 | rating: 'likes' | ||
805 | } | ||
806 | await makePutBodyRequest({ url: server.url, path: path + videoId + '/rate', token: server.accessToken, fields }) | ||
807 | }) | ||
808 | |||
809 | it('Should fail with a private video of another user', async function () { | ||
810 | const fields = { | ||
811 | rating: 'like' | ||
812 | } | ||
813 | await makePutBodyRequest({ | ||
814 | url: server.url, | ||
815 | path: path + privateVideo.uuid + '/rate', | ||
816 | token: userAccessToken, | ||
817 | fields, | ||
818 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
819 | }) | ||
820 | }) | ||
821 | |||
822 | it('Should succeed with the correct parameters', async function () { | ||
823 | const fields = { | ||
824 | rating: 'like' | ||
825 | } | ||
826 | await makePutBodyRequest({ | ||
827 | url: server.url, | ||
828 | path: path + videoId + '/rate', | ||
829 | token: server.accessToken, | ||
830 | fields, | ||
831 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
832 | }) | ||
833 | }) | ||
834 | }) | ||
835 | |||
836 | describe('When removing a video', function () { | ||
837 | it('Should have 404 with nothing', async function () { | ||
838 | await makeDeleteRequest({ | ||
839 | url: server.url, | ||
840 | path, | ||
841 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
842 | }) | ||
843 | }) | ||
844 | |||
845 | it('Should fail without a correct uuid', async function () { | ||
846 | await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
847 | }) | ||
848 | |||
849 | it('Should fail with a video which does not exist', async function () { | ||
850 | await server.videos.remove({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
851 | }) | ||
852 | |||
853 | it('Should fail with a video of another user without the appropriate right', async function () { | ||
854 | await server.videos.remove({ token: userAccessToken, id: video.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
855 | }) | ||
856 | |||
857 | it('Should fail with a video of another server') | ||
858 | |||
859 | it('Shoud report the appropriate error', async function () { | ||
860 | const body = await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
861 | const error = body as PeerTubeProblemDocument | ||
862 | |||
863 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo') | ||
864 | |||
865 | expect(error.type).to.equal('about:blank') | ||
866 | expect(error.title).to.equal('Bad Request') | ||
867 | |||
868 | expect(error.detail).to.equal('Incorrect request parameters: id') | ||
869 | expect(error.error).to.equal('Incorrect request parameters: id') | ||
870 | |||
871 | expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) | ||
872 | expect(error['invalid-params'].id).to.exist | ||
873 | }) | ||
874 | |||
875 | it('Should succeed with the correct parameters', async function () { | ||
876 | await server.videos.remove({ id: video.uuid }) | ||
877 | }) | ||
878 | }) | ||
879 | |||
880 | after(async function () { | ||
881 | await cleanupTests([ server ]) | ||
882 | }) | ||
883 | }) | ||
diff --git a/packages/tests/src/api/check-params/views.ts b/packages/tests/src/api/check-params/views.ts new file mode 100644 index 000000000..c454d4b80 --- /dev/null +++ b/packages/tests/src/api/check-params/views.ts | |||
@@ -0,0 +1,227 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test videos views', function () { | ||
14 | let servers: PeerTubeServer[] | ||
15 | let liveVideoId: string | ||
16 | let videoId: string | ||
17 | let remoteVideoId: string | ||
18 | let userAccessToken: string | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(120000) | ||
22 | |||
23 | servers = await createMultipleServers(2) | ||
24 | await setAccessTokensToServers(servers) | ||
25 | await setDefaultVideoChannel(servers) | ||
26 | |||
27 | await servers[0].config.enableLive({ allowReplay: false, transcoding: false }); | ||
28 | |||
29 | ({ uuid: videoId } = await servers[0].videos.quickUpload({ name: 'video' })); | ||
30 | ({ uuid: remoteVideoId } = await servers[1].videos.quickUpload({ name: 'video' })); | ||
31 | ({ uuid: liveVideoId } = await servers[0].live.create({ | ||
32 | fields: { | ||
33 | name: 'live', | ||
34 | privacy: VideoPrivacy.PUBLIC, | ||
35 | channelId: servers[0].store.channel.id | ||
36 | } | ||
37 | })) | ||
38 | |||
39 | userAccessToken = await servers[0].users.generateUserAndToken('user') | ||
40 | |||
41 | await doubleFollow(servers[0], servers[1]) | ||
42 | }) | ||
43 | |||
44 | describe('When viewing a video', async function () { | ||
45 | |||
46 | it('Should fail without current time', async function () { | ||
47 | await servers[0].views.view({ id: videoId, currentTime: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
48 | }) | ||
49 | |||
50 | it('Should fail with an invalid current time', async function () { | ||
51 | await servers[0].views.view({ id: videoId, currentTime: -1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
52 | await servers[0].views.view({ id: videoId, currentTime: 10, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
53 | }) | ||
54 | |||
55 | it('Should succeed with correct parameters', async function () { | ||
56 | await servers[0].views.view({ id: videoId, currentTime: 1 }) | ||
57 | }) | ||
58 | }) | ||
59 | |||
60 | describe('When getting overall stats', function () { | ||
61 | |||
62 | it('Should fail with a remote video', async function () { | ||
63 | await servers[0].videoStats.getOverallStats({ videoId: remoteVideoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
64 | }) | ||
65 | |||
66 | it('Should fail without token', async function () { | ||
67 | await servers[0].videoStats.getOverallStats({ videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
68 | }) | ||
69 | |||
70 | it('Should fail with another token', async function () { | ||
71 | await servers[0].videoStats.getOverallStats({ | ||
72 | videoId, | ||
73 | token: userAccessToken, | ||
74 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
75 | }) | ||
76 | }) | ||
77 | |||
78 | it('Should fail with an invalid start date', async function () { | ||
79 | await servers[0].videoStats.getOverallStats({ | ||
80 | videoId, | ||
81 | startDate: 'fake' as any, | ||
82 | endDate: new Date().toISOString(), | ||
83 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
84 | }) | ||
85 | }) | ||
86 | |||
87 | it('Should fail with an invalid end date', async function () { | ||
88 | await servers[0].videoStats.getOverallStats({ | ||
89 | videoId, | ||
90 | startDate: new Date().toISOString(), | ||
91 | endDate: 'fake' as any, | ||
92 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
93 | }) | ||
94 | }) | ||
95 | |||
96 | it('Should succeed with the correct parameters', async function () { | ||
97 | await servers[0].videoStats.getOverallStats({ | ||
98 | videoId, | ||
99 | startDate: new Date().toISOString(), | ||
100 | endDate: new Date().toISOString() | ||
101 | }) | ||
102 | }) | ||
103 | }) | ||
104 | |||
105 | describe('When getting timeserie stats', function () { | ||
106 | |||
107 | it('Should fail with a remote video', async function () { | ||
108 | await servers[0].videoStats.getTimeserieStats({ | ||
109 | videoId: remoteVideoId, | ||
110 | metric: 'viewers', | ||
111 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
112 | }) | ||
113 | }) | ||
114 | |||
115 | it('Should fail without token', async function () { | ||
116 | await servers[0].videoStats.getTimeserieStats({ | ||
117 | videoId, | ||
118 | token: null, | ||
119 | metric: 'viewers', | ||
120 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | it('Should fail with another token', async function () { | ||
125 | await servers[0].videoStats.getTimeserieStats({ | ||
126 | videoId, | ||
127 | token: userAccessToken, | ||
128 | metric: 'viewers', | ||
129 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
130 | }) | ||
131 | }) | ||
132 | |||
133 | it('Should fail with an invalid metric', async function () { | ||
134 | await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'hello' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
135 | }) | ||
136 | |||
137 | it('Should fail with an invalid start date', async function () { | ||
138 | await servers[0].videoStats.getTimeserieStats({ | ||
139 | videoId, | ||
140 | metric: 'viewers', | ||
141 | startDate: 'fake' as any, | ||
142 | endDate: new Date(), | ||
143 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
144 | }) | ||
145 | }) | ||
146 | |||
147 | it('Should fail with an invalid end date', async function () { | ||
148 | await servers[0].videoStats.getTimeserieStats({ | ||
149 | videoId, | ||
150 | metric: 'viewers', | ||
151 | startDate: new Date(), | ||
152 | endDate: 'fake' as any, | ||
153 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
154 | }) | ||
155 | }) | ||
156 | |||
157 | it('Should fail if start date is specified but not end date', async function () { | ||
158 | await servers[0].videoStats.getTimeserieStats({ | ||
159 | videoId, | ||
160 | metric: 'viewers', | ||
161 | startDate: new Date(), | ||
162 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
163 | }) | ||
164 | }) | ||
165 | |||
166 | it('Should fail if end date is specified but not start date', async function () { | ||
167 | await servers[0].videoStats.getTimeserieStats({ | ||
168 | videoId, | ||
169 | metric: 'viewers', | ||
170 | endDate: new Date(), | ||
171 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
172 | }) | ||
173 | }) | ||
174 | |||
175 | it('Should fail with a too big interval', async function () { | ||
176 | await servers[0].videoStats.getTimeserieStats({ | ||
177 | videoId, | ||
178 | metric: 'viewers', | ||
179 | startDate: new Date('2000-04-07T08:31:57.126Z'), | ||
180 | endDate: new Date(), | ||
181 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
182 | }) | ||
183 | }) | ||
184 | |||
185 | it('Should succeed with the correct parameters', async function () { | ||
186 | await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'viewers' }) | ||
187 | }) | ||
188 | }) | ||
189 | |||
190 | describe('When getting retention stats', function () { | ||
191 | |||
192 | it('Should fail with a remote video', async function () { | ||
193 | await servers[0].videoStats.getRetentionStats({ | ||
194 | videoId: remoteVideoId, | ||
195 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
196 | }) | ||
197 | }) | ||
198 | |||
199 | it('Should fail without token', async function () { | ||
200 | await servers[0].videoStats.getRetentionStats({ | ||
201 | videoId, | ||
202 | token: null, | ||
203 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
204 | }) | ||
205 | }) | ||
206 | |||
207 | it('Should fail with another token', async function () { | ||
208 | await servers[0].videoStats.getRetentionStats({ | ||
209 | videoId, | ||
210 | token: userAccessToken, | ||
211 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
212 | }) | ||
213 | }) | ||
214 | |||
215 | it('Should fail on live video', async function () { | ||
216 | await servers[0].videoStats.getRetentionStats({ videoId: liveVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
217 | }) | ||
218 | |||
219 | it('Should succeed with the correct parameters', async function () { | ||
220 | await servers[0].videoStats.getRetentionStats({ videoId }) | ||
221 | }) | ||
222 | }) | ||
223 | |||
224 | after(async function () { | ||
225 | await cleanupTests(servers) | ||
226 | }) | ||
227 | }) | ||
diff --git a/packages/tests/src/api/live/index.ts b/packages/tests/src/api/live/index.ts new file mode 100644 index 000000000..e61e6c611 --- /dev/null +++ b/packages/tests/src/api/live/index.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import './live-constraints.js' | ||
2 | import './live-fast-restream.js' | ||
3 | import './live-socket-messages.js' | ||
4 | import './live-permanent.js' | ||
5 | import './live-rtmps.js' | ||
6 | import './live-save-replay.js' | ||
7 | import './live.js' | ||
diff --git a/packages/tests/src/api/live/live-constraints.ts b/packages/tests/src/api/live/live-constraints.ts new file mode 100644 index 000000000..f62994cbd --- /dev/null +++ b/packages/tests/src/api/live/live-constraints.ts | |||
@@ -0,0 +1,237 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoError, UserVideoQuota, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | stopFfmpeg, | ||
15 | waitJobs, | ||
16 | waitUntilLiveReplacedByReplayOnAllServers, | ||
17 | waitUntilLiveWaitingOnAllServers | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | import { checkLiveCleanup } from '../../shared/live.js' | ||
20 | |||
21 | describe('Test live constraints', function () { | ||
22 | let servers: PeerTubeServer[] = [] | ||
23 | let userId: number | ||
24 | let userAccessToken: string | ||
25 | let userChannelId: number | ||
26 | |||
27 | async function createLiveWrapper (options: { replay: boolean, permanent: boolean }) { | ||
28 | const { replay, permanent } = options | ||
29 | |||
30 | const liveAttributes = { | ||
31 | name: 'user live', | ||
32 | channelId: userChannelId, | ||
33 | privacy: VideoPrivacy.PUBLIC, | ||
34 | saveReplay: replay, | ||
35 | replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined, | ||
36 | permanentLive: permanent | ||
37 | } | ||
38 | |||
39 | const { uuid } = await servers[0].live.create({ token: userAccessToken, fields: liveAttributes }) | ||
40 | return uuid | ||
41 | } | ||
42 | |||
43 | async function checkSaveReplay (videoId: string, resolutions = [ 720 ]) { | ||
44 | for (const server of servers) { | ||
45 | const video = await server.videos.get({ id: videoId }) | ||
46 | expect(video.isLive).to.be.false | ||
47 | expect(video.duration).to.be.greaterThan(0) | ||
48 | } | ||
49 | |||
50 | await checkLiveCleanup({ server: servers[0], permanent: false, videoUUID: videoId, savedResolutions: resolutions }) | ||
51 | } | ||
52 | |||
53 | function updateQuota (options: { total: number, daily: number }) { | ||
54 | return servers[0].users.update({ | ||
55 | userId, | ||
56 | videoQuota: options.total, | ||
57 | videoQuotaDaily: options.daily | ||
58 | }) | ||
59 | } | ||
60 | |||
61 | before(async function () { | ||
62 | this.timeout(120000) | ||
63 | |||
64 | servers = await createMultipleServers(2) | ||
65 | |||
66 | // Get the access tokens | ||
67 | await setAccessTokensToServers(servers) | ||
68 | await setDefaultVideoChannel(servers) | ||
69 | |||
70 | await servers[0].config.updateCustomSubConfig({ | ||
71 | newConfig: { | ||
72 | live: { | ||
73 | enabled: true, | ||
74 | allowReplay: true, | ||
75 | transcoding: { | ||
76 | enabled: false | ||
77 | } | ||
78 | } | ||
79 | } | ||
80 | }) | ||
81 | |||
82 | { | ||
83 | const res = await servers[0].users.generate('user1') | ||
84 | userId = res.userId | ||
85 | userChannelId = res.userChannelId | ||
86 | userAccessToken = res.token | ||
87 | |||
88 | await updateQuota({ total: 1, daily: -1 }) | ||
89 | } | ||
90 | |||
91 | // Server 1 and server 2 follow each other | ||
92 | await doubleFollow(servers[0], servers[1]) | ||
93 | }) | ||
94 | |||
95 | it('Should not have size limit if save replay is disabled', async function () { | ||
96 | this.timeout(60000) | ||
97 | |||
98 | const userVideoLiveoId = await createLiveWrapper({ replay: false, permanent: false }) | ||
99 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false }) | ||
100 | }) | ||
101 | |||
102 | it('Should have size limit depending on user global quota if save replay is enabled on non permanent live', async function () { | ||
103 | this.timeout(60000) | ||
104 | |||
105 | // Wait for user quota memoize cache invalidation | ||
106 | await wait(5000) | ||
107 | |||
108 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
109 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true }) | ||
110 | |||
111 | await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId) | ||
112 | await waitJobs(servers) | ||
113 | |||
114 | await checkSaveReplay(userVideoLiveoId) | ||
115 | |||
116 | const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId }) | ||
117 | expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED) | ||
118 | }) | ||
119 | |||
120 | it('Should have size limit depending on user global quota if save replay is enabled on a permanent live', async function () { | ||
121 | this.timeout(60000) | ||
122 | |||
123 | // Wait for user quota memoize cache invalidation | ||
124 | await wait(5000) | ||
125 | |||
126 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: true }) | ||
127 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true }) | ||
128 | |||
129 | await waitJobs(servers) | ||
130 | await waitUntilLiveWaitingOnAllServers(servers, userVideoLiveoId) | ||
131 | |||
132 | const session = await servers[0].live.findLatestSession({ videoId: userVideoLiveoId }) | ||
133 | expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED) | ||
134 | }) | ||
135 | |||
136 | it('Should have size limit depending on user daily quota if save replay is enabled', async function () { | ||
137 | this.timeout(60000) | ||
138 | |||
139 | // Wait for user quota memoize cache invalidation | ||
140 | await wait(5000) | ||
141 | |||
142 | await updateQuota({ total: -1, daily: 1 }) | ||
143 | |||
144 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
145 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true }) | ||
146 | |||
147 | await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId) | ||
148 | await waitJobs(servers) | ||
149 | |||
150 | await checkSaveReplay(userVideoLiveoId) | ||
151 | |||
152 | const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId }) | ||
153 | expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED) | ||
154 | }) | ||
155 | |||
156 | it('Should succeed without quota limit', async function () { | ||
157 | this.timeout(60000) | ||
158 | |||
159 | // Wait for user quota memoize cache invalidation | ||
160 | await wait(5000) | ||
161 | |||
162 | await updateQuota({ total: 10 * 1000 * 1000, daily: -1 }) | ||
163 | |||
164 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
165 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false }) | ||
166 | }) | ||
167 | |||
168 | it('Should have the same quota in admin and as a user', async function () { | ||
169 | this.timeout(120000) | ||
170 | |||
171 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
172 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ token: userAccessToken, videoId: userVideoLiveoId }) | ||
173 | |||
174 | await servers[0].live.waitUntilPublished({ videoId: userVideoLiveoId }) | ||
175 | // Wait previous live cleanups | ||
176 | await wait(3000) | ||
177 | |||
178 | const baseQuota = await servers[0].users.getMyQuotaUsed({ token: userAccessToken }) | ||
179 | |||
180 | let quotaUser: UserVideoQuota | ||
181 | |||
182 | do { | ||
183 | await wait(500) | ||
184 | |||
185 | quotaUser = await servers[0].users.getMyQuotaUsed({ token: userAccessToken }) | ||
186 | } while (quotaUser.videoQuotaUsed <= baseQuota.videoQuotaUsed) | ||
187 | |||
188 | const { data } = await servers[0].users.list() | ||
189 | const quotaAdmin = data.find(u => u.username === 'user1') | ||
190 | |||
191 | expect(quotaUser.videoQuotaUsed).to.be.above(baseQuota.videoQuotaUsed) | ||
192 | expect(quotaUser.videoQuotaUsedDaily).to.be.above(baseQuota.videoQuotaUsedDaily) | ||
193 | |||
194 | expect(quotaAdmin.videoQuotaUsed).to.be.above(baseQuota.videoQuotaUsed) | ||
195 | expect(quotaAdmin.videoQuotaUsedDaily).to.be.above(baseQuota.videoQuotaUsedDaily) | ||
196 | |||
197 | expect(quotaUser.videoQuotaUsed).to.be.above(10) | ||
198 | expect(quotaUser.videoQuotaUsedDaily).to.be.above(10) | ||
199 | expect(quotaAdmin.videoQuotaUsed).to.be.above(10) | ||
200 | expect(quotaAdmin.videoQuotaUsedDaily).to.be.above(10) | ||
201 | |||
202 | await stopFfmpeg(ffmpegCommand) | ||
203 | }) | ||
204 | |||
205 | it('Should have max duration limit', async function () { | ||
206 | this.timeout(240000) | ||
207 | |||
208 | await servers[0].config.updateCustomSubConfig({ | ||
209 | newConfig: { | ||
210 | live: { | ||
211 | enabled: true, | ||
212 | allowReplay: true, | ||
213 | maxDuration: 15, | ||
214 | transcoding: { | ||
215 | enabled: true, | ||
216 | resolutions: ConfigCommand.getCustomConfigResolutions(true) | ||
217 | } | ||
218 | } | ||
219 | } | ||
220 | }) | ||
221 | |||
222 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
223 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true }) | ||
224 | |||
225 | await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId) | ||
226 | await waitJobs(servers) | ||
227 | |||
228 | await checkSaveReplay(userVideoLiveoId, [ 720, 480, 360, 240, 144 ]) | ||
229 | |||
230 | const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId }) | ||
231 | expect(session.error).to.equal(LiveVideoError.DURATION_EXCEEDED) | ||
232 | }) | ||
233 | |||
234 | after(async function () { | ||
235 | await cleanupTests(servers) | ||
236 | }) | ||
237 | }) | ||
diff --git a/packages/tests/src/api/live/live-fast-restream.ts b/packages/tests/src/api/live/live-fast-restream.ts new file mode 100644 index 000000000..d34b00cbe --- /dev/null +++ b/packages/tests/src/api/live/live-fast-restream.ts | |||
@@ -0,0 +1,153 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultVideoChannel, | ||
12 | stopFfmpeg, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Fast restream in live', function () { | ||
17 | let server: PeerTubeServer | ||
18 | |||
19 | async function createLiveWrapper (options: { permanent: boolean, replay: boolean }) { | ||
20 | const attributes: LiveVideoCreate = { | ||
21 | channelId: server.store.channel.id, | ||
22 | privacy: VideoPrivacy.PUBLIC, | ||
23 | name: 'my super live', | ||
24 | saveReplay: options.replay, | ||
25 | replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined, | ||
26 | permanentLive: options.permanent | ||
27 | } | ||
28 | |||
29 | const { uuid } = await server.live.create({ fields: attributes }) | ||
30 | return uuid | ||
31 | } | ||
32 | |||
33 | async function fastRestreamWrapper ({ replay }: { replay: boolean }) { | ||
34 | const liveVideoUUID = await createLiveWrapper({ permanent: true, replay }) | ||
35 | await waitJobs([ server ]) | ||
36 | |||
37 | const rtmpOptions = { | ||
38 | videoId: liveVideoUUID, | ||
39 | copyCodecs: true, | ||
40 | fixtureName: 'video_short.mp4' | ||
41 | } | ||
42 | |||
43 | // Streaming session #1 | ||
44 | let ffmpegCommand = await server.live.sendRTMPStreamInVideo(rtmpOptions) | ||
45 | await server.live.waitUntilPublished({ videoId: liveVideoUUID }) | ||
46 | |||
47 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
48 | const session1PlaylistId = video.streamingPlaylists[0].id | ||
49 | |||
50 | await stopFfmpeg(ffmpegCommand) | ||
51 | await server.live.waitUntilWaiting({ videoId: liveVideoUUID }) | ||
52 | |||
53 | // Streaming session #2 | ||
54 | ffmpegCommand = await server.live.sendRTMPStreamInVideo(rtmpOptions) | ||
55 | |||
56 | let hasNewPlaylist = false | ||
57 | do { | ||
58 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
59 | hasNewPlaylist = video.streamingPlaylists.length === 1 && video.streamingPlaylists[0].id !== session1PlaylistId | ||
60 | |||
61 | await wait(100) | ||
62 | } while (!hasNewPlaylist) | ||
63 | |||
64 | await server.live.waitUntilSegmentGeneration({ | ||
65 | server, | ||
66 | videoUUID: liveVideoUUID, | ||
67 | segment: 1, | ||
68 | playlistNumber: 0 | ||
69 | }) | ||
70 | |||
71 | return { ffmpegCommand, liveVideoUUID } | ||
72 | } | ||
73 | |||
74 | async function ensureLastLiveWorks (liveId: string) { | ||
75 | // Equivalent to PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY | ||
76 | for (let i = 0; i < 100; i++) { | ||
77 | const video = await server.videos.get({ id: liveId }) | ||
78 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
79 | |||
80 | try { | ||
81 | await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 }) | ||
82 | await server.streamingPlaylists.get({ url: video.streamingPlaylists[0].playlistUrl }) | ||
83 | await server.streamingPlaylists.getSegmentSha256({ url: video.streamingPlaylists[0].segmentsSha256Url }) | ||
84 | } catch (err) { | ||
85 | // FIXME: try to debug error in CI "Unexpected end of JSON input" | ||
86 | console.error(err) | ||
87 | throw err | ||
88 | } | ||
89 | |||
90 | await wait(100) | ||
91 | } | ||
92 | } | ||
93 | |||
94 | async function runTest (replay: boolean) { | ||
95 | const { ffmpegCommand, liveVideoUUID } = await fastRestreamWrapper({ replay }) | ||
96 | |||
97 | // TODO: remove, we try to debug a test timeout failure here | ||
98 | console.log('Ensuring last live works') | ||
99 | |||
100 | await ensureLastLiveWorks(liveVideoUUID) | ||
101 | |||
102 | await stopFfmpeg(ffmpegCommand) | ||
103 | await server.live.waitUntilWaiting({ videoId: liveVideoUUID }) | ||
104 | |||
105 | // Wait for replays | ||
106 | await waitJobs([ server ]) | ||
107 | |||
108 | const { total, data: sessions } = await server.live.listSessions({ videoId: liveVideoUUID }) | ||
109 | |||
110 | expect(total).to.equal(2) | ||
111 | expect(sessions).to.have.lengthOf(2) | ||
112 | |||
113 | for (const session of sessions) { | ||
114 | expect(session.error).to.be.null | ||
115 | |||
116 | if (replay) { | ||
117 | expect(session.replayVideo).to.exist | ||
118 | |||
119 | await server.videos.get({ id: session.replayVideo.uuid }) | ||
120 | } else { | ||
121 | expect(session.replayVideo).to.not.exist | ||
122 | } | ||
123 | } | ||
124 | } | ||
125 | |||
126 | before(async function () { | ||
127 | this.timeout(120000) | ||
128 | |||
129 | const env = { PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY: '10000' } | ||
130 | server = await createSingleServer(1, {}, { env }) | ||
131 | |||
132 | // Get the access tokens | ||
133 | await setAccessTokensToServers([ server ]) | ||
134 | await setDefaultVideoChannel([ server ]) | ||
135 | |||
136 | await server.config.enableMinimumTranscoding({ webVideo: false, hls: true }) | ||
137 | await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) | ||
138 | }) | ||
139 | |||
140 | it('Should correctly fast restream in a permanent live with and without save replay', async function () { | ||
141 | this.timeout(480000) | ||
142 | |||
143 | // A test can take a long time, so prefer to run them in parallel | ||
144 | await Promise.all([ | ||
145 | runTest(true), | ||
146 | runTest(false) | ||
147 | ]) | ||
148 | }) | ||
149 | |||
150 | after(async function () { | ||
151 | await cleanupTests([ server ]) | ||
152 | }) | ||
153 | }) | ||
diff --git a/packages/tests/src/api/live/live-permanent.ts b/packages/tests/src/api/live/live-permanent.ts new file mode 100644 index 000000000..4ffcc7ed4 --- /dev/null +++ b/packages/tests/src/api/live/live-permanent.ts | |||
@@ -0,0 +1,204 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoCreate, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models' | ||
6 | import { checkLiveCleanup } from '@tests/shared/live.js' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | ConfigCommand, | ||
10 | createMultipleServers, | ||
11 | doubleFollow, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | setDefaultVideoChannel, | ||
15 | stopFfmpeg, | ||
16 | waitJobs | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | |||
19 | describe('Permanent live', function () { | ||
20 | let servers: PeerTubeServer[] = [] | ||
21 | let videoUUID: string | ||
22 | |||
23 | async function createLiveWrapper (permanentLive: boolean) { | ||
24 | const attributes: LiveVideoCreate = { | ||
25 | channelId: servers[0].store.channel.id, | ||
26 | privacy: VideoPrivacy.PUBLIC, | ||
27 | name: 'my super live', | ||
28 | saveReplay: false, | ||
29 | permanentLive | ||
30 | } | ||
31 | |||
32 | const { uuid } = await servers[0].live.create({ fields: attributes }) | ||
33 | return uuid | ||
34 | } | ||
35 | |||
36 | async function checkVideoState (videoId: string, state: VideoStateType) { | ||
37 | for (const server of servers) { | ||
38 | const video = await server.videos.get({ id: videoId }) | ||
39 | expect(video.state.id).to.equal(state) | ||
40 | } | ||
41 | } | ||
42 | |||
43 | before(async function () { | ||
44 | this.timeout(120000) | ||
45 | |||
46 | servers = await createMultipleServers(2) | ||
47 | |||
48 | // Get the access tokens | ||
49 | await setAccessTokensToServers(servers) | ||
50 | await setDefaultVideoChannel(servers) | ||
51 | |||
52 | // Server 1 and server 2 follow each other | ||
53 | await doubleFollow(servers[0], servers[1]) | ||
54 | |||
55 | await servers[0].config.updateCustomSubConfig({ | ||
56 | newConfig: { | ||
57 | live: { | ||
58 | enabled: true, | ||
59 | allowReplay: true, | ||
60 | maxDuration: -1, | ||
61 | transcoding: { | ||
62 | enabled: true, | ||
63 | resolutions: ConfigCommand.getCustomConfigResolutions(true) | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | }) | ||
68 | }) | ||
69 | |||
70 | it('Should create a non permanent live and update it to be a permanent live', async function () { | ||
71 | this.timeout(20000) | ||
72 | |||
73 | const videoUUID = await createLiveWrapper(false) | ||
74 | |||
75 | { | ||
76 | const live = await servers[0].live.get({ videoId: videoUUID }) | ||
77 | expect(live.permanentLive).to.be.false | ||
78 | } | ||
79 | |||
80 | await servers[0].live.update({ videoId: videoUUID, fields: { permanentLive: true } }) | ||
81 | |||
82 | { | ||
83 | const live = await servers[0].live.get({ videoId: videoUUID }) | ||
84 | expect(live.permanentLive).to.be.true | ||
85 | } | ||
86 | }) | ||
87 | |||
88 | it('Should create a permanent live', async function () { | ||
89 | this.timeout(20000) | ||
90 | |||
91 | videoUUID = await createLiveWrapper(true) | ||
92 | |||
93 | const live = await servers[0].live.get({ videoId: videoUUID }) | ||
94 | expect(live.permanentLive).to.be.true | ||
95 | |||
96 | await waitJobs(servers) | ||
97 | }) | ||
98 | |||
99 | it('Should stream into this permanent live', async function () { | ||
100 | this.timeout(240_000) | ||
101 | |||
102 | const beforePublication = new Date() | ||
103 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID }) | ||
104 | |||
105 | for (const server of servers) { | ||
106 | await server.live.waitUntilPublished({ videoId: videoUUID }) | ||
107 | } | ||
108 | |||
109 | await checkVideoState(videoUUID, VideoState.PUBLISHED) | ||
110 | |||
111 | for (const server of servers) { | ||
112 | const video = await server.videos.get({ id: videoUUID }) | ||
113 | expect(new Date(video.publishedAt)).greaterThan(beforePublication) | ||
114 | } | ||
115 | |||
116 | await stopFfmpeg(ffmpegCommand) | ||
117 | await servers[0].live.waitUntilWaiting({ videoId: videoUUID }) | ||
118 | |||
119 | await waitJobs(servers) | ||
120 | }) | ||
121 | |||
122 | it('Should have cleaned up this live', async function () { | ||
123 | this.timeout(40000) | ||
124 | |||
125 | await wait(5000) | ||
126 | await waitJobs(servers) | ||
127 | |||
128 | for (const server of servers) { | ||
129 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
130 | |||
131 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(0) | ||
132 | } | ||
133 | |||
134 | await checkLiveCleanup({ server: servers[0], permanent: true, videoUUID }) | ||
135 | }) | ||
136 | |||
137 | it('Should have set this live to waiting for live state', async function () { | ||
138 | this.timeout(20000) | ||
139 | |||
140 | await checkVideoState(videoUUID, VideoState.WAITING_FOR_LIVE) | ||
141 | }) | ||
142 | |||
143 | it('Should be able to stream again in the permanent live', async function () { | ||
144 | this.timeout(60000) | ||
145 | |||
146 | await servers[0].config.updateCustomSubConfig({ | ||
147 | newConfig: { | ||
148 | live: { | ||
149 | enabled: true, | ||
150 | allowReplay: true, | ||
151 | maxDuration: -1, | ||
152 | transcoding: { | ||
153 | enabled: true, | ||
154 | resolutions: ConfigCommand.getCustomConfigResolutions(false) | ||
155 | } | ||
156 | } | ||
157 | } | ||
158 | }) | ||
159 | |||
160 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID }) | ||
161 | |||
162 | for (const server of servers) { | ||
163 | await server.live.waitUntilPublished({ videoId: videoUUID }) | ||
164 | } | ||
165 | |||
166 | await checkVideoState(videoUUID, VideoState.PUBLISHED) | ||
167 | |||
168 | const count = await servers[0].live.countPlaylists({ videoUUID }) | ||
169 | // master playlist and 720p playlist | ||
170 | expect(count).to.equal(2) | ||
171 | |||
172 | await stopFfmpeg(ffmpegCommand) | ||
173 | }) | ||
174 | |||
175 | it('Should have appropriate sessions', async function () { | ||
176 | this.timeout(60000) | ||
177 | |||
178 | await servers[0].live.waitUntilWaiting({ videoId: videoUUID }) | ||
179 | |||
180 | const { data, total } = await servers[0].live.listSessions({ videoId: videoUUID }) | ||
181 | expect(total).to.equal(2) | ||
182 | expect(data).to.have.lengthOf(2) | ||
183 | |||
184 | for (const session of data) { | ||
185 | expect(session.startDate).to.exist | ||
186 | expect(session.endDate).to.exist | ||
187 | |||
188 | expect(session.error).to.not.exist | ||
189 | } | ||
190 | }) | ||
191 | |||
192 | it('Should remove the live and have cleaned up the directory', async function () { | ||
193 | this.timeout(60000) | ||
194 | |||
195 | await servers[0].videos.remove({ id: videoUUID }) | ||
196 | await waitJobs(servers) | ||
197 | |||
198 | await checkLiveCleanup({ server: servers[0], permanent: true, videoUUID }) | ||
199 | }) | ||
200 | |||
201 | after(async function () { | ||
202 | await cleanupTests(servers) | ||
203 | }) | ||
204 | }) | ||
diff --git a/packages/tests/src/api/live/live-rtmps.ts b/packages/tests/src/api/live/live-rtmps.ts new file mode 100644 index 000000000..4ab59ed4c --- /dev/null +++ b/packages/tests/src/api/live/live-rtmps.ts | |||
@@ -0,0 +1,143 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | sendRTMPStream, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | stopFfmpeg, | ||
14 | testFfmpegStreamError, | ||
15 | waitUntilLivePublishedOnAllServers | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('Test live RTMPS', function () { | ||
19 | let server: PeerTubeServer | ||
20 | let rtmpUrl: string | ||
21 | let rtmpsUrl: string | ||
22 | |||
23 | async function createLiveWrapper () { | ||
24 | const liveAttributes = { | ||
25 | name: 'live', | ||
26 | channelId: server.store.channel.id, | ||
27 | privacy: VideoPrivacy.PUBLIC, | ||
28 | saveReplay: false | ||
29 | } | ||
30 | |||
31 | const { uuid } = await server.live.create({ fields: liveAttributes }) | ||
32 | |||
33 | const live = await server.live.get({ videoId: uuid }) | ||
34 | const video = await server.videos.get({ id: uuid }) | ||
35 | |||
36 | return Object.assign(video, live) | ||
37 | } | ||
38 | |||
39 | before(async function () { | ||
40 | this.timeout(120000) | ||
41 | |||
42 | server = await createSingleServer(1) | ||
43 | |||
44 | // Get the access tokens | ||
45 | await setAccessTokensToServers([ server ]) | ||
46 | await setDefaultVideoChannel([ server ]) | ||
47 | |||
48 | await server.config.updateCustomSubConfig({ | ||
49 | newConfig: { | ||
50 | live: { | ||
51 | enabled: true, | ||
52 | allowReplay: true, | ||
53 | transcoding: { | ||
54 | enabled: false | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | }) | ||
59 | |||
60 | rtmpUrl = 'rtmp://' + server.hostname + ':' + server.rtmpPort + '/live' | ||
61 | rtmpsUrl = 'rtmps://' + server.hostname + ':' + server.rtmpsPort + '/live' | ||
62 | }) | ||
63 | |||
64 | it('Should enable RTMPS endpoint only', async function () { | ||
65 | this.timeout(240000) | ||
66 | |||
67 | await server.kill() | ||
68 | await server.run({ | ||
69 | live: { | ||
70 | rtmp: { | ||
71 | enabled: false | ||
72 | }, | ||
73 | rtmps: { | ||
74 | enabled: true, | ||
75 | port: server.rtmpsPort, | ||
76 | key_file: buildAbsoluteFixturePath('rtmps.key'), | ||
77 | cert_file: buildAbsoluteFixturePath('rtmps.cert') | ||
78 | } | ||
79 | } | ||
80 | }) | ||
81 | |||
82 | { | ||
83 | const liveVideo = await createLiveWrapper() | ||
84 | |||
85 | expect(liveVideo.rtmpUrl).to.not.exist | ||
86 | expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl) | ||
87 | |||
88 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey }) | ||
89 | await testFfmpegStreamError(command, true) | ||
90 | } | ||
91 | |||
92 | { | ||
93 | const liveVideo = await createLiveWrapper() | ||
94 | |||
95 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey }) | ||
96 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
97 | await stopFfmpeg(command) | ||
98 | } | ||
99 | }) | ||
100 | |||
101 | it('Should enable both RTMP and RTMPS', async function () { | ||
102 | this.timeout(240000) | ||
103 | |||
104 | await server.kill() | ||
105 | await server.run({ | ||
106 | live: { | ||
107 | rtmp: { | ||
108 | enabled: true, | ||
109 | port: server.rtmpPort | ||
110 | }, | ||
111 | rtmps: { | ||
112 | enabled: true, | ||
113 | port: server.rtmpsPort, | ||
114 | key_file: buildAbsoluteFixturePath('rtmps.key'), | ||
115 | cert_file: buildAbsoluteFixturePath('rtmps.cert') | ||
116 | } | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | { | ||
121 | const liveVideo = await createLiveWrapper() | ||
122 | |||
123 | expect(liveVideo.rtmpUrl).to.equal(rtmpUrl) | ||
124 | expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl) | ||
125 | |||
126 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey }) | ||
127 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
128 | await stopFfmpeg(command) | ||
129 | } | ||
130 | |||
131 | { | ||
132 | const liveVideo = await createLiveWrapper() | ||
133 | |||
134 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey }) | ||
135 | await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid) | ||
136 | await stopFfmpeg(command) | ||
137 | } | ||
138 | }) | ||
139 | |||
140 | after(async function () { | ||
141 | await cleanupTests([ server ]) | ||
142 | }) | ||
143 | }) | ||
diff --git a/packages/tests/src/api/live/live-save-replay.ts b/packages/tests/src/api/live/live-save-replay.ts new file mode 100644 index 000000000..84135365b --- /dev/null +++ b/packages/tests/src/api/live/live-save-replay.ts | |||
@@ -0,0 +1,583 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | HttpStatusCode, | ||
8 | HttpStatusCodeType, | ||
9 | LiveVideoCreate, | ||
10 | LiveVideoError, | ||
11 | VideoPrivacy, | ||
12 | VideoPrivacyType, | ||
13 | VideoState, | ||
14 | VideoStateType | ||
15 | } from '@peertube/peertube-models' | ||
16 | import { checkLiveCleanup } from '@tests/shared/live.js' | ||
17 | import { | ||
18 | cleanupTests, | ||
19 | ConfigCommand, | ||
20 | createMultipleServers, | ||
21 | doubleFollow, | ||
22 | findExternalSavedVideo, | ||
23 | PeerTubeServer, | ||
24 | setAccessTokensToServers, | ||
25 | setDefaultVideoChannel, | ||
26 | stopFfmpeg, | ||
27 | testFfmpegStreamError, | ||
28 | waitJobs, | ||
29 | waitUntilLivePublishedOnAllServers, | ||
30 | waitUntilLiveReplacedByReplayOnAllServers, | ||
31 | waitUntilLiveWaitingOnAllServers | ||
32 | } from '@peertube/peertube-server-commands' | ||
33 | |||
34 | describe('Save replay setting', function () { | ||
35 | let servers: PeerTubeServer[] = [] | ||
36 | let liveVideoUUID: string | ||
37 | let ffmpegCommand: FfmpegCommand | ||
38 | |||
39 | async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) { | ||
40 | if (liveVideoUUID) { | ||
41 | try { | ||
42 | await servers[0].videos.remove({ id: liveVideoUUID }) | ||
43 | await waitJobs(servers) | ||
44 | } catch {} | ||
45 | } | ||
46 | |||
47 | const attributes: LiveVideoCreate = { | ||
48 | channelId: servers[0].store.channel.id, | ||
49 | privacy: VideoPrivacy.PUBLIC, | ||
50 | name: 'live'.repeat(30), | ||
51 | saveReplay: options.replay, | ||
52 | replaySettings: options.replaySettings, | ||
53 | permanentLive: options.permanent | ||
54 | } | ||
55 | |||
56 | const { uuid } = await servers[0].live.create({ fields: attributes }) | ||
57 | return uuid | ||
58 | } | ||
59 | |||
60 | async function publishLive (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) { | ||
61 | liveVideoUUID = await createLiveWrapper(options) | ||
62 | |||
63 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
64 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
65 | |||
66 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) | ||
67 | |||
68 | await waitJobs(servers) | ||
69 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
70 | |||
71 | return { ffmpegCommand, liveDetails } | ||
72 | } | ||
73 | |||
74 | async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) { | ||
75 | const { ffmpegCommand, liveDetails } = await publishLive(options) | ||
76 | |||
77 | await Promise.all([ | ||
78 | servers[0].videos.remove({ id: liveVideoUUID }), | ||
79 | testFfmpegStreamError(ffmpegCommand, true) | ||
80 | ]) | ||
81 | |||
82 | await waitJobs(servers) | ||
83 | await wait(5000) | ||
84 | await waitJobs(servers) | ||
85 | |||
86 | return { liveDetails } | ||
87 | } | ||
88 | |||
89 | async function publishLiveAndBlacklist (options: { | ||
90 | permanent: boolean | ||
91 | replay: boolean | ||
92 | replaySettings?: { privacy: VideoPrivacyType } | ||
93 | }) { | ||
94 | const { ffmpegCommand, liveDetails } = await publishLive(options) | ||
95 | |||
96 | await Promise.all([ | ||
97 | servers[0].blacklist.add({ videoId: liveVideoUUID, reason: 'bad live', unfederate: true }), | ||
98 | testFfmpegStreamError(ffmpegCommand, true) | ||
99 | ]) | ||
100 | |||
101 | await waitJobs(servers) | ||
102 | await wait(5000) | ||
103 | await waitJobs(servers) | ||
104 | |||
105 | return { liveDetails } | ||
106 | } | ||
107 | |||
108 | async function checkVideosExist (videoId: string, existsInList: boolean, expectedStatus?: HttpStatusCodeType) { | ||
109 | for (const server of servers) { | ||
110 | const length = existsInList ? 1 : 0 | ||
111 | |||
112 | const { data, total } = await server.videos.list() | ||
113 | expect(data).to.have.lengthOf(length) | ||
114 | expect(total).to.equal(length) | ||
115 | |||
116 | if (expectedStatus) { | ||
117 | await server.videos.get({ id: videoId, expectedStatus }) | ||
118 | } | ||
119 | } | ||
120 | } | ||
121 | |||
122 | async function checkVideoState (videoId: string, state: VideoStateType) { | ||
123 | for (const server of servers) { | ||
124 | const video = await server.videos.get({ id: videoId }) | ||
125 | expect(video.state.id).to.equal(state) | ||
126 | } | ||
127 | } | ||
128 | |||
129 | async function checkVideoPrivacy (videoId: string, privacy: VideoPrivacyType) { | ||
130 | for (const server of servers) { | ||
131 | const video = await server.videos.get({ id: videoId }) | ||
132 | expect(video.privacy.id).to.equal(privacy) | ||
133 | } | ||
134 | } | ||
135 | |||
136 | before(async function () { | ||
137 | this.timeout(120000) | ||
138 | |||
139 | servers = await createMultipleServers(2) | ||
140 | |||
141 | // Get the access tokens | ||
142 | await setAccessTokensToServers(servers) | ||
143 | await setDefaultVideoChannel(servers) | ||
144 | |||
145 | // Server 1 and server 2 follow each other | ||
146 | await doubleFollow(servers[0], servers[1]) | ||
147 | |||
148 | await servers[0].config.updateCustomSubConfig({ | ||
149 | newConfig: { | ||
150 | live: { | ||
151 | enabled: true, | ||
152 | allowReplay: true, | ||
153 | maxDuration: -1, | ||
154 | transcoding: { | ||
155 | enabled: false, | ||
156 | resolutions: ConfigCommand.getCustomConfigResolutions(true) | ||
157 | } | ||
158 | } | ||
159 | } | ||
160 | }) | ||
161 | }) | ||
162 | |||
163 | describe('With save replay disabled', function () { | ||
164 | let sessionStartDateMin: Date | ||
165 | let sessionStartDateMax: Date | ||
166 | let sessionEndDateMin: Date | ||
167 | |||
168 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
169 | this.timeout(40000) | ||
170 | |||
171 | liveVideoUUID = await createLiveWrapper({ permanent: false, replay: false }) | ||
172 | |||
173 | await waitJobs(servers) | ||
174 | |||
175 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
176 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
177 | }) | ||
178 | |||
179 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
180 | this.timeout(120000) | ||
181 | |||
182 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
183 | |||
184 | sessionStartDateMin = new Date() | ||
185 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
186 | sessionStartDateMax = new Date() | ||
187 | |||
188 | await waitJobs(servers) | ||
189 | |||
190 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
191 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
192 | }) | ||
193 | |||
194 | it('Should correctly delete the video files after the stream ended', async function () { | ||
195 | this.timeout(120000) | ||
196 | |||
197 | sessionEndDateMin = new Date() | ||
198 | await stopFfmpeg(ffmpegCommand) | ||
199 | |||
200 | for (const server of servers) { | ||
201 | await server.live.waitUntilEnded({ videoId: liveVideoUUID }) | ||
202 | } | ||
203 | await waitJobs(servers) | ||
204 | |||
205 | // Live still exist, but cannot be played anymore | ||
206 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
207 | await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED) | ||
208 | |||
209 | // No resolutions saved since we did not save replay | ||
210 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
211 | }) | ||
212 | |||
213 | it('Should have appropriate ended session', async function () { | ||
214 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | ||
215 | expect(total).to.equal(1) | ||
216 | expect(data).to.have.lengthOf(1) | ||
217 | |||
218 | const session = data[0] | ||
219 | |||
220 | const startDate = new Date(session.startDate) | ||
221 | expect(startDate).to.be.above(sessionStartDateMin) | ||
222 | expect(startDate).to.be.below(sessionStartDateMax) | ||
223 | |||
224 | expect(session.endDate).to.exist | ||
225 | expect(new Date(session.endDate)).to.be.above(sessionEndDateMin) | ||
226 | |||
227 | expect(session.saveReplay).to.be.false | ||
228 | expect(session.error).to.not.exist | ||
229 | expect(session.replayVideo).to.not.exist | ||
230 | }) | ||
231 | |||
232 | it('Should correctly terminate the stream on blacklist and delete the live', async function () { | ||
233 | this.timeout(120000) | ||
234 | |||
235 | await publishLiveAndBlacklist({ permanent: false, replay: false }) | ||
236 | |||
237 | await checkVideosExist(liveVideoUUID, false) | ||
238 | |||
239 | await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
240 | await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
241 | |||
242 | await wait(5000) | ||
243 | await waitJobs(servers) | ||
244 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
245 | }) | ||
246 | |||
247 | it('Should have blacklisted session error', async function () { | ||
248 | const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID }) | ||
249 | expect(session.startDate).to.exist | ||
250 | expect(session.endDate).to.exist | ||
251 | |||
252 | expect(session.error).to.equal(LiveVideoError.BLACKLISTED) | ||
253 | expect(session.replayVideo).to.not.exist | ||
254 | }) | ||
255 | |||
256 | it('Should correctly terminate the stream on delete and delete the video', async function () { | ||
257 | this.timeout(120000) | ||
258 | |||
259 | await publishLiveAndDelete({ permanent: false, replay: false }) | ||
260 | |||
261 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | ||
262 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
263 | }) | ||
264 | }) | ||
265 | |||
266 | describe('With save replay enabled on non permanent live', function () { | ||
267 | |||
268 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
269 | this.timeout(120000) | ||
270 | |||
271 | liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } }) | ||
272 | |||
273 | await waitJobs(servers) | ||
274 | |||
275 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
276 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
277 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
278 | }) | ||
279 | |||
280 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
281 | this.timeout(120000) | ||
282 | |||
283 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
284 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
285 | |||
286 | await waitJobs(servers) | ||
287 | |||
288 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
289 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
290 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
291 | }) | ||
292 | |||
293 | it('Should correctly have saved the live and federated it after the streaming', async function () { | ||
294 | this.timeout(120000) | ||
295 | |||
296 | const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID }) | ||
297 | expect(session.endDate).to.not.exist | ||
298 | expect(session.endingProcessed).to.be.false | ||
299 | expect(session.saveReplay).to.be.true | ||
300 | expect(session.replaySettings).to.exist | ||
301 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) | ||
302 | |||
303 | await stopFfmpeg(ffmpegCommand) | ||
304 | |||
305 | await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID) | ||
306 | await waitJobs(servers) | ||
307 | |||
308 | // Live has been transcoded | ||
309 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
310 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
311 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED) | ||
312 | }) | ||
313 | |||
314 | it('Should find the replay live session', async function () { | ||
315 | const session = await servers[0].live.getReplaySession({ videoId: liveVideoUUID }) | ||
316 | |||
317 | expect(session).to.exist | ||
318 | |||
319 | expect(session.startDate).to.exist | ||
320 | expect(session.endDate).to.exist | ||
321 | |||
322 | expect(session.error).to.not.exist | ||
323 | expect(session.saveReplay).to.be.true | ||
324 | expect(session.endingProcessed).to.be.true | ||
325 | expect(session.replaySettings).to.exist | ||
326 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) | ||
327 | |||
328 | expect(session.replayVideo).to.exist | ||
329 | expect(session.replayVideo.id).to.exist | ||
330 | expect(session.replayVideo.shortUUID).to.exist | ||
331 | expect(session.replayVideo.uuid).to.equal(liveVideoUUID) | ||
332 | }) | ||
333 | |||
334 | it('Should update the saved live and correctly federate the updated attributes', async function () { | ||
335 | this.timeout(120000) | ||
336 | |||
337 | await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated', privacy: VideoPrivacy.PUBLIC } }) | ||
338 | await waitJobs(servers) | ||
339 | |||
340 | for (const server of servers) { | ||
341 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
342 | expect(video.name).to.equal('video updated') | ||
343 | expect(video.isLive).to.be.false | ||
344 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) | ||
345 | } | ||
346 | }) | ||
347 | |||
348 | it('Should have cleaned up the live files', async function () { | ||
349 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false, savedResolutions: [ 720 ] }) | ||
350 | }) | ||
351 | |||
352 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | ||
353 | this.timeout(120000) | ||
354 | |||
355 | await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }) | ||
356 | |||
357 | await checkVideosExist(liveVideoUUID, false) | ||
358 | |||
359 | await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
360 | await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
361 | |||
362 | await wait(5000) | ||
363 | await waitJobs(servers) | ||
364 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false, savedResolutions: [ 720 ] }) | ||
365 | }) | ||
366 | |||
367 | it('Should correctly terminate the stream on delete and delete the video', async function () { | ||
368 | this.timeout(120000) | ||
369 | |||
370 | await publishLiveAndDelete({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }) | ||
371 | |||
372 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | ||
373 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
374 | }) | ||
375 | }) | ||
376 | |||
377 | describe('With save replay enabled on permanent live', function () { | ||
378 | let lastReplayUUID: string | ||
379 | |||
380 | describe('With a first live and its replay', function () { | ||
381 | |||
382 | it('Should correctly create and federate the "waiting for stream" live', async function () { | ||
383 | this.timeout(120000) | ||
384 | |||
385 | liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } }) | ||
386 | |||
387 | await waitJobs(servers) | ||
388 | |||
389 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200) | ||
390 | await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE) | ||
391 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
392 | }) | ||
393 | |||
394 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
395 | this.timeout(120000) | ||
396 | |||
397 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
398 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
399 | |||
400 | await waitJobs(servers) | ||
401 | |||
402 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
403 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
404 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
405 | }) | ||
406 | |||
407 | it('Should correctly have saved the live and federated it after the streaming', async function () { | ||
408 | this.timeout(120000) | ||
409 | |||
410 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) | ||
411 | |||
412 | await stopFfmpeg(ffmpegCommand) | ||
413 | |||
414 | await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) | ||
415 | await waitJobs(servers) | ||
416 | |||
417 | const video = await findExternalSavedVideo(servers[0], liveDetails) | ||
418 | expect(video).to.exist | ||
419 | |||
420 | for (const server of servers) { | ||
421 | await server.videos.get({ id: video.uuid }) | ||
422 | } | ||
423 | |||
424 | lastReplayUUID = video.uuid | ||
425 | }) | ||
426 | |||
427 | it('Should have appropriate ended session and replay live session', async function () { | ||
428 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | ||
429 | expect(total).to.equal(1) | ||
430 | expect(data).to.have.lengthOf(1) | ||
431 | |||
432 | const sessionFromLive = data[0] | ||
433 | const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) | ||
434 | |||
435 | for (const session of [ sessionFromLive, sessionFromReplay ]) { | ||
436 | expect(session.startDate).to.exist | ||
437 | expect(session.endDate).to.exist | ||
438 | |||
439 | expect(session.replaySettings).to.exist | ||
440 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED) | ||
441 | |||
442 | expect(session.error).to.not.exist | ||
443 | |||
444 | expect(session.replayVideo).to.exist | ||
445 | expect(session.replayVideo.id).to.exist | ||
446 | expect(session.replayVideo.shortUUID).to.exist | ||
447 | expect(session.replayVideo.uuid).to.equal(lastReplayUUID) | ||
448 | } | ||
449 | }) | ||
450 | |||
451 | it('Should have the first live replay with correct settings', async function () { | ||
452 | await checkVideosExist(lastReplayUUID, false, HttpStatusCode.OK_200) | ||
453 | await checkVideoState(lastReplayUUID, VideoState.PUBLISHED) | ||
454 | await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.UNLISTED) | ||
455 | }) | ||
456 | }) | ||
457 | |||
458 | describe('With a second live and its replay', function () { | ||
459 | |||
460 | it('Should update the replay settings', async function () { | ||
461 | await servers[0].live.update({ videoId: liveVideoUUID, fields: { replaySettings: { privacy: VideoPrivacy.PUBLIC } } }) | ||
462 | await waitJobs(servers) | ||
463 | |||
464 | const live = await servers[0].live.get({ videoId: liveVideoUUID }) | ||
465 | |||
466 | expect(live.saveReplay).to.be.true | ||
467 | expect(live.replaySettings).to.exist | ||
468 | expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
469 | |||
470 | }) | ||
471 | |||
472 | it('Should correctly have updated the live and federated it when streaming in the live', async function () { | ||
473 | this.timeout(120000) | ||
474 | |||
475 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
476 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
477 | |||
478 | await waitJobs(servers) | ||
479 | |||
480 | await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200) | ||
481 | await checkVideoState(liveVideoUUID, VideoState.PUBLISHED) | ||
482 | await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC) | ||
483 | }) | ||
484 | |||
485 | it('Should correctly have saved the live and federated it after the streaming', async function () { | ||
486 | this.timeout(120000) | ||
487 | |||
488 | const liveDetails = await servers[0].videos.get({ id: liveVideoUUID }) | ||
489 | |||
490 | await stopFfmpeg(ffmpegCommand) | ||
491 | |||
492 | await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID) | ||
493 | await waitJobs(servers) | ||
494 | |||
495 | const video = await findExternalSavedVideo(servers[0], liveDetails) | ||
496 | expect(video).to.exist | ||
497 | |||
498 | for (const server of servers) { | ||
499 | await server.videos.get({ id: video.uuid }) | ||
500 | } | ||
501 | |||
502 | lastReplayUUID = video.uuid | ||
503 | }) | ||
504 | |||
505 | it('Should have appropriate ended session and replay live session', async function () { | ||
506 | const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID }) | ||
507 | expect(total).to.equal(2) | ||
508 | expect(data).to.have.lengthOf(2) | ||
509 | |||
510 | const sessionFromLive = data[1] | ||
511 | const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID }) | ||
512 | |||
513 | for (const session of [ sessionFromLive, sessionFromReplay ]) { | ||
514 | expect(session.startDate).to.exist | ||
515 | expect(session.endDate).to.exist | ||
516 | |||
517 | expect(session.replaySettings).to.exist | ||
518 | expect(session.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
519 | |||
520 | expect(session.error).to.not.exist | ||
521 | |||
522 | expect(session.replayVideo).to.exist | ||
523 | expect(session.replayVideo.id).to.exist | ||
524 | expect(session.replayVideo.shortUUID).to.exist | ||
525 | expect(session.replayVideo.uuid).to.equal(lastReplayUUID) | ||
526 | } | ||
527 | }) | ||
528 | |||
529 | it('Should have the first live replay with correct settings', async function () { | ||
530 | await checkVideosExist(lastReplayUUID, true, HttpStatusCode.OK_200) | ||
531 | await checkVideoState(lastReplayUUID, VideoState.PUBLISHED) | ||
532 | await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC) | ||
533 | }) | ||
534 | |||
535 | it('Should have cleaned up the live files', async function () { | ||
536 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
537 | }) | ||
538 | |||
539 | it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () { | ||
540 | this.timeout(120000) | ||
541 | |||
542 | await servers[0].videos.remove({ id: lastReplayUUID }) | ||
543 | const { liveDetails } = await publishLiveAndBlacklist({ | ||
544 | permanent: true, | ||
545 | replay: true, | ||
546 | replaySettings: { privacy: VideoPrivacy.PUBLIC } | ||
547 | }) | ||
548 | |||
549 | const replay = await findExternalSavedVideo(servers[0], liveDetails) | ||
550 | expect(replay).to.exist | ||
551 | |||
552 | for (const videoId of [ liveVideoUUID, replay.uuid ]) { | ||
553 | await checkVideosExist(videoId, false) | ||
554 | |||
555 | await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
556 | await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
557 | } | ||
558 | |||
559 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
560 | }) | ||
561 | |||
562 | it('Should correctly terminate the stream on delete and not save the video', async function () { | ||
563 | this.timeout(120000) | ||
564 | |||
565 | const { liveDetails } = await publishLiveAndDelete({ | ||
566 | permanent: true, | ||
567 | replay: true, | ||
568 | replaySettings: { privacy: VideoPrivacy.PUBLIC } | ||
569 | }) | ||
570 | |||
571 | const replay = await findExternalSavedVideo(servers[0], liveDetails) | ||
572 | expect(replay).to.not.exist | ||
573 | |||
574 | await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404) | ||
575 | await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false }) | ||
576 | }) | ||
577 | }) | ||
578 | }) | ||
579 | |||
580 | after(async function () { | ||
581 | await cleanupTests(servers) | ||
582 | }) | ||
583 | }) | ||
diff --git a/packages/tests/src/api/live/live-socket-messages.ts b/packages/tests/src/api/live/live-socket-messages.ts new file mode 100644 index 000000000..80bae154c --- /dev/null +++ b/packages/tests/src/api/live/live-socket-messages.ts | |||
@@ -0,0 +1,186 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoEventPayload, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | stopFfmpeg, | ||
14 | waitJobs, | ||
15 | waitUntilLivePublishedOnAllServers | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('Test live socket messages', function () { | ||
19 | let servers: PeerTubeServer[] = [] | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(120000) | ||
23 | |||
24 | servers = await createMultipleServers(2) | ||
25 | |||
26 | // Get the access tokens | ||
27 | await setAccessTokensToServers(servers) | ||
28 | await setDefaultVideoChannel(servers) | ||
29 | |||
30 | await servers[0].config.updateCustomSubConfig({ | ||
31 | newConfig: { | ||
32 | live: { | ||
33 | enabled: true, | ||
34 | allowReplay: true, | ||
35 | transcoding: { | ||
36 | enabled: false | ||
37 | } | ||
38 | } | ||
39 | } | ||
40 | }) | ||
41 | |||
42 | // Server 1 and server 2 follow each other | ||
43 | await doubleFollow(servers[0], servers[1]) | ||
44 | }) | ||
45 | |||
46 | describe('Live socket messages', function () { | ||
47 | |||
48 | async function createLiveWrapper () { | ||
49 | const liveAttributes = { | ||
50 | name: 'live video', | ||
51 | channelId: servers[0].store.channel.id, | ||
52 | privacy: VideoPrivacy.PUBLIC | ||
53 | } | ||
54 | |||
55 | const { uuid } = await servers[0].live.create({ fields: liveAttributes }) | ||
56 | return uuid | ||
57 | } | ||
58 | |||
59 | it('Should correctly send a message when the live starts and ends', async function () { | ||
60 | this.timeout(60000) | ||
61 | |||
62 | const localStateChanges: VideoStateType[] = [] | ||
63 | const remoteStateChanges: VideoStateType[] = [] | ||
64 | |||
65 | const liveVideoUUID = await createLiveWrapper() | ||
66 | await waitJobs(servers) | ||
67 | |||
68 | { | ||
69 | const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID }) | ||
70 | |||
71 | const localSocket = servers[0].socketIO.getLiveNotificationSocket() | ||
72 | localSocket.on('state-change', data => localStateChanges.push(data.state)) | ||
73 | localSocket.emit('subscribe', { videoId }) | ||
74 | } | ||
75 | |||
76 | { | ||
77 | const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID }) | ||
78 | |||
79 | const remoteSocket = servers[1].socketIO.getLiveNotificationSocket() | ||
80 | remoteSocket.on('state-change', data => remoteStateChanges.push(data.state)) | ||
81 | remoteSocket.emit('subscribe', { videoId }) | ||
82 | } | ||
83 | |||
84 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
85 | |||
86 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
87 | await waitJobs(servers) | ||
88 | |||
89 | for (const stateChanges of [ localStateChanges, remoteStateChanges ]) { | ||
90 | expect(stateChanges).to.have.length.at.least(1) | ||
91 | expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.PUBLISHED) | ||
92 | } | ||
93 | |||
94 | await stopFfmpeg(ffmpegCommand) | ||
95 | |||
96 | for (const server of servers) { | ||
97 | await server.live.waitUntilEnded({ videoId: liveVideoUUID }) | ||
98 | } | ||
99 | await waitJobs(servers) | ||
100 | |||
101 | for (const stateChanges of [ localStateChanges, remoteStateChanges ]) { | ||
102 | expect(stateChanges).to.have.length.at.least(2) | ||
103 | expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.LIVE_ENDED) | ||
104 | } | ||
105 | }) | ||
106 | |||
107 | it('Should correctly send views change notification', async function () { | ||
108 | this.timeout(60000) | ||
109 | |||
110 | let localLastVideoViews = 0 | ||
111 | let remoteLastVideoViews = 0 | ||
112 | |||
113 | const liveVideoUUID = await createLiveWrapper() | ||
114 | await waitJobs(servers) | ||
115 | |||
116 | { | ||
117 | const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID }) | ||
118 | |||
119 | const localSocket = servers[0].socketIO.getLiveNotificationSocket() | ||
120 | localSocket.on('views-change', (data: LiveVideoEventPayload) => { localLastVideoViews = data.viewers }) | ||
121 | localSocket.emit('subscribe', { videoId }) | ||
122 | } | ||
123 | |||
124 | { | ||
125 | const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID }) | ||
126 | |||
127 | const remoteSocket = servers[1].socketIO.getLiveNotificationSocket() | ||
128 | remoteSocket.on('views-change', (data: LiveVideoEventPayload) => { remoteLastVideoViews = data.viewers }) | ||
129 | remoteSocket.emit('subscribe', { videoId }) | ||
130 | } | ||
131 | |||
132 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
133 | |||
134 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
135 | await waitJobs(servers) | ||
136 | |||
137 | expect(localLastVideoViews).to.equal(0) | ||
138 | expect(remoteLastVideoViews).to.equal(0) | ||
139 | |||
140 | await servers[0].views.simulateView({ id: liveVideoUUID }) | ||
141 | await servers[1].views.simulateView({ id: liveVideoUUID }) | ||
142 | |||
143 | await waitJobs(servers) | ||
144 | |||
145 | expect(localLastVideoViews).to.equal(2) | ||
146 | expect(remoteLastVideoViews).to.equal(2) | ||
147 | |||
148 | await stopFfmpeg(ffmpegCommand) | ||
149 | }) | ||
150 | |||
151 | it('Should not receive a notification after unsubscribe', async function () { | ||
152 | this.timeout(120000) | ||
153 | |||
154 | const stateChanges: VideoStateType[] = [] | ||
155 | |||
156 | const liveVideoUUID = await createLiveWrapper() | ||
157 | await waitJobs(servers) | ||
158 | |||
159 | const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID }) | ||
160 | |||
161 | const socket = servers[0].socketIO.getLiveNotificationSocket() | ||
162 | socket.on('state-change', data => stateChanges.push(data.state)) | ||
163 | socket.emit('subscribe', { videoId }) | ||
164 | |||
165 | const command = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID }) | ||
166 | |||
167 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
168 | await waitJobs(servers) | ||
169 | |||
170 | // Notifier waits before sending a notification | ||
171 | await wait(10000) | ||
172 | |||
173 | expect(stateChanges).to.have.lengthOf(1) | ||
174 | socket.emit('unsubscribe', { videoId }) | ||
175 | |||
176 | await stopFfmpeg(command) | ||
177 | await waitJobs(servers) | ||
178 | |||
179 | expect(stateChanges).to.have.lengthOf(1) | ||
180 | }) | ||
181 | }) | ||
182 | |||
183 | after(async function () { | ||
184 | await cleanupTests(servers) | ||
185 | }) | ||
186 | }) | ||
diff --git a/packages/tests/src/api/live/live.ts b/packages/tests/src/api/live/live.ts new file mode 100644 index 000000000..20804f889 --- /dev/null +++ b/packages/tests/src/api/live/live.ts | |||
@@ -0,0 +1,766 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename, join } from 'path' | ||
5 | import { getAllFiles, wait } from '@peertube/peertube-core-utils' | ||
6 | import { ffprobePromise, getVideoStream } from '@peertube/peertube-ffmpeg' | ||
7 | import { | ||
8 | HttpStatusCode, | ||
9 | LiveVideo, | ||
10 | LiveVideoCreate, | ||
11 | LiveVideoLatencyMode, | ||
12 | VideoDetails, | ||
13 | VideoPrivacy, | ||
14 | VideoState, | ||
15 | VideoStreamingPlaylistType | ||
16 | } from '@peertube/peertube-models' | ||
17 | import { | ||
18 | cleanupTests, | ||
19 | createMultipleServers, | ||
20 | doubleFollow, | ||
21 | killallServers, | ||
22 | LiveCommand, | ||
23 | makeGetRequest, | ||
24 | makeRawRequest, | ||
25 | PeerTubeServer, | ||
26 | sendRTMPStream, | ||
27 | setAccessTokensToServers, | ||
28 | setDefaultVideoChannel, | ||
29 | stopFfmpeg, | ||
30 | testFfmpegStreamError, | ||
31 | waitJobs, | ||
32 | waitUntilLivePublishedOnAllServers | ||
33 | } from '@peertube/peertube-server-commands' | ||
34 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
35 | import { testLiveVideoResolutions } from '@tests/shared/live.js' | ||
36 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
37 | |||
38 | describe('Test live', function () { | ||
39 | let servers: PeerTubeServer[] = [] | ||
40 | let commands: LiveCommand[] | ||
41 | |||
42 | before(async function () { | ||
43 | this.timeout(120000) | ||
44 | |||
45 | servers = await createMultipleServers(2) | ||
46 | |||
47 | // Get the access tokens | ||
48 | await setAccessTokensToServers(servers) | ||
49 | await setDefaultVideoChannel(servers) | ||
50 | |||
51 | await servers[0].config.updateCustomSubConfig({ | ||
52 | newConfig: { | ||
53 | live: { | ||
54 | enabled: true, | ||
55 | allowReplay: true, | ||
56 | latencySetting: { | ||
57 | enabled: true | ||
58 | }, | ||
59 | transcoding: { | ||
60 | enabled: false | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | }) | ||
65 | |||
66 | // Server 1 and server 2 follow each other | ||
67 | await doubleFollow(servers[0], servers[1]) | ||
68 | |||
69 | commands = servers.map(s => s.live) | ||
70 | }) | ||
71 | |||
72 | describe('Live creation, update and delete', function () { | ||
73 | let liveVideoUUID: string | ||
74 | |||
75 | it('Should create a live with the appropriate parameters', async function () { | ||
76 | this.timeout(20000) | ||
77 | |||
78 | const attributes: LiveVideoCreate = { | ||
79 | category: 1, | ||
80 | licence: 2, | ||
81 | language: 'fr', | ||
82 | description: 'super live description', | ||
83 | support: 'support field', | ||
84 | channelId: servers[0].store.channel.id, | ||
85 | nsfw: false, | ||
86 | waitTranscoding: false, | ||
87 | name: 'my super live', | ||
88 | tags: [ 'tag1', 'tag2' ], | ||
89 | commentsEnabled: false, | ||
90 | downloadEnabled: false, | ||
91 | saveReplay: true, | ||
92 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
93 | latencyMode: LiveVideoLatencyMode.SMALL_LATENCY, | ||
94 | privacy: VideoPrivacy.PUBLIC, | ||
95 | previewfile: 'video_short1-preview.webm.jpg', | ||
96 | thumbnailfile: 'video_short1.webm.jpg' | ||
97 | } | ||
98 | |||
99 | const live = await commands[0].create({ fields: attributes }) | ||
100 | liveVideoUUID = live.uuid | ||
101 | |||
102 | await waitJobs(servers) | ||
103 | |||
104 | for (const server of servers) { | ||
105 | const video = await server.videos.get({ id: liveVideoUUID }) | ||
106 | |||
107 | expect(video.category.id).to.equal(1) | ||
108 | expect(video.licence.id).to.equal(2) | ||
109 | expect(video.language.id).to.equal('fr') | ||
110 | expect(video.description).to.equal('super live description') | ||
111 | expect(video.support).to.equal('support field') | ||
112 | |||
113 | expect(video.channel.name).to.equal(servers[0].store.channel.name) | ||
114 | expect(video.channel.host).to.equal(servers[0].store.channel.host) | ||
115 | |||
116 | expect(video.isLive).to.be.true | ||
117 | |||
118 | expect(video.nsfw).to.be.false | ||
119 | expect(video.waitTranscoding).to.be.false | ||
120 | expect(video.name).to.equal('my super live') | ||
121 | expect(video.tags).to.deep.equal([ 'tag1', 'tag2' ]) | ||
122 | expect(video.commentsEnabled).to.be.false | ||
123 | expect(video.downloadEnabled).to.be.false | ||
124 | expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC) | ||
125 | |||
126 | await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath) | ||
127 | await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath) | ||
128 | |||
129 | const live = await server.live.get({ videoId: liveVideoUUID }) | ||
130 | |||
131 | if (server.url === servers[0].url) { | ||
132 | expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') | ||
133 | expect(live.streamKey).to.not.be.empty | ||
134 | |||
135 | expect(live.replaySettings).to.exist | ||
136 | expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
137 | } else { | ||
138 | expect(live.rtmpUrl).to.not.exist | ||
139 | expect(live.streamKey).to.not.exist | ||
140 | } | ||
141 | |||
142 | expect(live.saveReplay).to.be.true | ||
143 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY) | ||
144 | } | ||
145 | }) | ||
146 | |||
147 | it('Should have a default preview and thumbnail', async function () { | ||
148 | this.timeout(20000) | ||
149 | |||
150 | const attributes: LiveVideoCreate = { | ||
151 | name: 'default live thumbnail', | ||
152 | channelId: servers[0].store.channel.id, | ||
153 | privacy: VideoPrivacy.UNLISTED, | ||
154 | nsfw: true | ||
155 | } | ||
156 | |||
157 | const live = await commands[0].create({ fields: attributes }) | ||
158 | const videoId = live.uuid | ||
159 | |||
160 | await waitJobs(servers) | ||
161 | |||
162 | for (const server of servers) { | ||
163 | const video = await server.videos.get({ id: videoId }) | ||
164 | expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED) | ||
165 | expect(video.nsfw).to.be.true | ||
166 | |||
167 | await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
168 | await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
169 | } | ||
170 | }) | ||
171 | |||
172 | it('Should not have the live listed since nobody streams into', async function () { | ||
173 | for (const server of servers) { | ||
174 | const { total, data } = await server.videos.list() | ||
175 | |||
176 | expect(total).to.equal(0) | ||
177 | expect(data).to.have.lengthOf(0) | ||
178 | } | ||
179 | }) | ||
180 | |||
181 | it('Should not be able to update a live of another server', async function () { | ||
182 | await commands[1].update({ videoId: liveVideoUUID, fields: { saveReplay: false }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
183 | }) | ||
184 | |||
185 | it('Should update the live', async function () { | ||
186 | await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } }) | ||
187 | await waitJobs(servers) | ||
188 | }) | ||
189 | |||
190 | it('Have the live updated', async function () { | ||
191 | for (const server of servers) { | ||
192 | const live = await server.live.get({ videoId: liveVideoUUID }) | ||
193 | |||
194 | if (server.url === servers[0].url) { | ||
195 | expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live') | ||
196 | expect(live.streamKey).to.not.be.empty | ||
197 | } else { | ||
198 | expect(live.rtmpUrl).to.not.exist | ||
199 | expect(live.streamKey).to.not.exist | ||
200 | } | ||
201 | |||
202 | expect(live.saveReplay).to.be.false | ||
203 | expect(live.replaySettings).to.not.exist | ||
204 | expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT) | ||
205 | } | ||
206 | }) | ||
207 | |||
208 | it('Delete the live', async function () { | ||
209 | await servers[0].videos.remove({ id: liveVideoUUID }) | ||
210 | await waitJobs(servers) | ||
211 | }) | ||
212 | |||
213 | it('Should have the live deleted', async function () { | ||
214 | for (const server of servers) { | ||
215 | await server.videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
216 | await server.live.get({ videoId: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
217 | } | ||
218 | }) | ||
219 | }) | ||
220 | |||
221 | describe('Live filters', function () { | ||
222 | let ffmpegCommand: any | ||
223 | let liveVideoId: string | ||
224 | let vodVideoId: string | ||
225 | |||
226 | before(async function () { | ||
227 | this.timeout(240000) | ||
228 | |||
229 | vodVideoId = (await servers[0].videos.quickUpload({ name: 'vod video' })).uuid | ||
230 | |||
231 | const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].store.channel.id } | ||
232 | const live = await commands[0].create({ fields: liveOptions }) | ||
233 | liveVideoId = live.uuid | ||
234 | |||
235 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
236 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
237 | await waitJobs(servers) | ||
238 | }) | ||
239 | |||
240 | it('Should only display lives', async function () { | ||
241 | const { data, total } = await servers[0].videos.list({ isLive: true }) | ||
242 | |||
243 | expect(total).to.equal(1) | ||
244 | expect(data).to.have.lengthOf(1) | ||
245 | expect(data[0].name).to.equal('live') | ||
246 | }) | ||
247 | |||
248 | it('Should not display lives', async function () { | ||
249 | const { data, total } = await servers[0].videos.list({ isLive: false }) | ||
250 | |||
251 | expect(total).to.equal(1) | ||
252 | expect(data).to.have.lengthOf(1) | ||
253 | expect(data[0].name).to.equal('vod video') | ||
254 | }) | ||
255 | |||
256 | it('Should display my lives', async function () { | ||
257 | this.timeout(60000) | ||
258 | |||
259 | await stopFfmpeg(ffmpegCommand) | ||
260 | await waitJobs(servers) | ||
261 | |||
262 | const { data } = await servers[0].videos.listMyVideos({ isLive: true }) | ||
263 | |||
264 | const result = data.every(v => v.isLive) | ||
265 | expect(result).to.be.true | ||
266 | }) | ||
267 | |||
268 | it('Should not display my lives', async function () { | ||
269 | const { data } = await servers[0].videos.listMyVideos({ isLive: false }) | ||
270 | |||
271 | const result = data.every(v => !v.isLive) | ||
272 | expect(result).to.be.true | ||
273 | }) | ||
274 | |||
275 | after(async function () { | ||
276 | await servers[0].videos.remove({ id: vodVideoId }) | ||
277 | await servers[0].videos.remove({ id: liveVideoId }) | ||
278 | }) | ||
279 | }) | ||
280 | |||
281 | describe('Stream checks', function () { | ||
282 | let liveVideo: LiveVideo & VideoDetails | ||
283 | let rtmpUrl: string | ||
284 | |||
285 | before(function () { | ||
286 | rtmpUrl = 'rtmp://' + servers[0].hostname + ':' + servers[0].rtmpPort + '' | ||
287 | }) | ||
288 | |||
289 | async function createLiveWrapper () { | ||
290 | const liveAttributes = { | ||
291 | name: 'user live', | ||
292 | channelId: servers[0].store.channel.id, | ||
293 | privacy: VideoPrivacy.PUBLIC, | ||
294 | saveReplay: false | ||
295 | } | ||
296 | |||
297 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
298 | |||
299 | const live = await commands[0].get({ videoId: uuid }) | ||
300 | const video = await servers[0].videos.get({ id: uuid }) | ||
301 | |||
302 | return Object.assign(video, live) | ||
303 | } | ||
304 | |||
305 | it('Should not allow a stream without the appropriate path', async function () { | ||
306 | this.timeout(60000) | ||
307 | |||
308 | liveVideo = await createLiveWrapper() | ||
309 | |||
310 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/bad-live', streamKey: liveVideo.streamKey }) | ||
311 | await testFfmpegStreamError(command, true) | ||
312 | }) | ||
313 | |||
314 | it('Should not allow a stream without the appropriate stream key', async function () { | ||
315 | this.timeout(60000) | ||
316 | |||
317 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: 'bad-stream-key' }) | ||
318 | await testFfmpegStreamError(command, true) | ||
319 | }) | ||
320 | |||
321 | it('Should succeed with the correct params', async function () { | ||
322 | this.timeout(60000) | ||
323 | |||
324 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
325 | await testFfmpegStreamError(command, false) | ||
326 | }) | ||
327 | |||
328 | it('Should list this live now someone stream into it', async function () { | ||
329 | for (const server of servers) { | ||
330 | const { total, data } = await server.videos.list() | ||
331 | |||
332 | expect(total).to.equal(1) | ||
333 | expect(data).to.have.lengthOf(1) | ||
334 | |||
335 | const video = data[0] | ||
336 | expect(video.name).to.equal('user live') | ||
337 | expect(video.isLive).to.be.true | ||
338 | } | ||
339 | }) | ||
340 | |||
341 | it('Should not allow a stream on a live that was blacklisted', async function () { | ||
342 | this.timeout(60000) | ||
343 | |||
344 | liveVideo = await createLiveWrapper() | ||
345 | |||
346 | await servers[0].blacklist.add({ videoId: liveVideo.uuid }) | ||
347 | |||
348 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
349 | await testFfmpegStreamError(command, true) | ||
350 | }) | ||
351 | |||
352 | it('Should not allow a stream on a live that was deleted', async function () { | ||
353 | this.timeout(60000) | ||
354 | |||
355 | liveVideo = await createLiveWrapper() | ||
356 | |||
357 | await servers[0].videos.remove({ id: liveVideo.uuid }) | ||
358 | |||
359 | const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey }) | ||
360 | await testFfmpegStreamError(command, true) | ||
361 | }) | ||
362 | }) | ||
363 | |||
364 | describe('Live transcoding', function () { | ||
365 | let liveVideoId: string | ||
366 | let sqlCommandServer1: SQLCommand | ||
367 | |||
368 | async function createLiveWrapper (saveReplay: boolean) { | ||
369 | const liveAttributes = { | ||
370 | name: 'live video', | ||
371 | channelId: servers[0].store.channel.id, | ||
372 | privacy: VideoPrivacy.PUBLIC, | ||
373 | saveReplay, | ||
374 | replaySettings: saveReplay | ||
375 | ? { privacy: VideoPrivacy.PUBLIC } | ||
376 | : undefined | ||
377 | } | ||
378 | |||
379 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
380 | return uuid | ||
381 | } | ||
382 | |||
383 | function updateConf (resolutions: number[]) { | ||
384 | return servers[0].config.updateCustomSubConfig({ | ||
385 | newConfig: { | ||
386 | live: { | ||
387 | enabled: true, | ||
388 | allowReplay: true, | ||
389 | maxDuration: -1, | ||
390 | transcoding: { | ||
391 | enabled: true, | ||
392 | resolutions: { | ||
393 | '144p': resolutions.includes(144), | ||
394 | '240p': resolutions.includes(240), | ||
395 | '360p': resolutions.includes(360), | ||
396 | '480p': resolutions.includes(480), | ||
397 | '720p': resolutions.includes(720), | ||
398 | '1080p': resolutions.includes(1080), | ||
399 | '2160p': resolutions.includes(2160) | ||
400 | } | ||
401 | } | ||
402 | } | ||
403 | } | ||
404 | }) | ||
405 | } | ||
406 | |||
407 | before(async function () { | ||
408 | await updateConf([]) | ||
409 | |||
410 | sqlCommandServer1 = new SQLCommand(servers[0]) | ||
411 | }) | ||
412 | |||
413 | it('Should enable transcoding without additional resolutions', async function () { | ||
414 | this.timeout(120000) | ||
415 | |||
416 | liveVideoId = await createLiveWrapper(false) | ||
417 | |||
418 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
419 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
420 | await waitJobs(servers) | ||
421 | |||
422 | await testLiveVideoResolutions({ | ||
423 | originServer: servers[0], | ||
424 | sqlCommand: sqlCommandServer1, | ||
425 | servers, | ||
426 | liveVideoId, | ||
427 | resolutions: [ 720 ], | ||
428 | transcoded: true | ||
429 | }) | ||
430 | |||
431 | await stopFfmpeg(ffmpegCommand) | ||
432 | }) | ||
433 | |||
434 | it('Should transcode audio only RTMP stream', async function () { | ||
435 | this.timeout(120000) | ||
436 | |||
437 | liveVideoId = await createLiveWrapper(false) | ||
438 | |||
439 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short_no_audio.mp4' }) | ||
440 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
441 | await waitJobs(servers) | ||
442 | |||
443 | await stopFfmpeg(ffmpegCommand) | ||
444 | }) | ||
445 | |||
446 | it('Should enable transcoding with some resolutions', async function () { | ||
447 | this.timeout(240000) | ||
448 | |||
449 | const resolutions = [ 240, 480 ] | ||
450 | await updateConf(resolutions) | ||
451 | liveVideoId = await createLiveWrapper(false) | ||
452 | |||
453 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
454 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
455 | await waitJobs(servers) | ||
456 | |||
457 | await testLiveVideoResolutions({ | ||
458 | originServer: servers[0], | ||
459 | sqlCommand: sqlCommandServer1, | ||
460 | servers, | ||
461 | liveVideoId, | ||
462 | resolutions: resolutions.concat([ 720 ]), | ||
463 | transcoded: true | ||
464 | }) | ||
465 | |||
466 | await stopFfmpeg(ffmpegCommand) | ||
467 | }) | ||
468 | |||
469 | it('Should correctly set the appropriate bitrate depending on the input', async function () { | ||
470 | this.timeout(120000) | ||
471 | |||
472 | liveVideoId = await createLiveWrapper(false) | ||
473 | |||
474 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ | ||
475 | videoId: liveVideoId, | ||
476 | fixtureName: 'video_short.mp4', | ||
477 | copyCodecs: true | ||
478 | }) | ||
479 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
480 | await waitJobs(servers) | ||
481 | |||
482 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
483 | |||
484 | const masterPlaylist = video.streamingPlaylists[0].playlistUrl | ||
485 | const probe = await ffprobePromise(masterPlaylist) | ||
486 | |||
487 | const bitrates = probe.streams.map(s => parseInt(s.tags.variant_bitrate)) | ||
488 | for (const bitrate of bitrates) { | ||
489 | expect(bitrate).to.exist | ||
490 | expect(isNaN(bitrate)).to.be.false | ||
491 | expect(bitrate).to.be.below(61_000_000) // video_short.mp4 bitrate | ||
492 | } | ||
493 | |||
494 | await stopFfmpeg(ffmpegCommand) | ||
495 | }) | ||
496 | |||
497 | it('Should enable transcoding with some resolutions and correctly save them', async function () { | ||
498 | this.timeout(500_000) | ||
499 | |||
500 | const resolutions = [ 240, 360, 720 ] | ||
501 | |||
502 | await updateConf(resolutions) | ||
503 | liveVideoId = await createLiveWrapper(true) | ||
504 | |||
505 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
506 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
507 | await waitJobs(servers) | ||
508 | |||
509 | await testLiveVideoResolutions({ | ||
510 | originServer: servers[0], | ||
511 | sqlCommand: sqlCommandServer1, | ||
512 | servers, | ||
513 | liveVideoId, | ||
514 | resolutions, | ||
515 | transcoded: true | ||
516 | }) | ||
517 | |||
518 | await stopFfmpeg(ffmpegCommand) | ||
519 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
520 | |||
521 | await waitJobs(servers) | ||
522 | |||
523 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
524 | |||
525 | const maxBitrateLimits = { | ||
526 | 720: 6500 * 1000, // 60FPS | ||
527 | 360: 1250 * 1000, | ||
528 | 240: 700 * 1000 | ||
529 | } | ||
530 | |||
531 | const minBitrateLimits = { | ||
532 | 720: 4800 * 1000, | ||
533 | 360: 1000 * 1000, | ||
534 | 240: 550 * 1000 | ||
535 | } | ||
536 | |||
537 | for (const server of servers) { | ||
538 | const video = await server.videos.get({ id: liveVideoId }) | ||
539 | |||
540 | expect(video.state.id).to.equal(VideoState.PUBLISHED) | ||
541 | expect(video.duration).to.be.greaterThan(1) | ||
542 | expect(video.files).to.have.lengthOf(0) | ||
543 | |||
544 | const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) | ||
545 | await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
546 | await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
547 | |||
548 | // We should have generated random filenames | ||
549 | expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8') | ||
550 | expect(basename(hlsPlaylist.segmentsSha256Url)).to.not.equal('segments-sha256.json') | ||
551 | |||
552 | expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length) | ||
553 | |||
554 | for (const resolution of resolutions) { | ||
555 | const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) | ||
556 | |||
557 | expect(file).to.exist | ||
558 | expect(file.size).to.be.greaterThan(1) | ||
559 | |||
560 | if (resolution >= 720) { | ||
561 | expect(file.fps).to.be.approximately(60, 10) | ||
562 | } else { | ||
563 | expect(file.fps).to.be.approximately(30, 3) | ||
564 | } | ||
565 | |||
566 | const filename = basename(file.fileUrl) | ||
567 | expect(filename).to.not.contain(video.uuid) | ||
568 | |||
569 | const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename)) | ||
570 | |||
571 | const probe = await ffprobePromise(segmentPath) | ||
572 | const videoStream = await getVideoStream(segmentPath, probe) | ||
573 | |||
574 | expect(probe.format.bit_rate).to.be.below(maxBitrateLimits[videoStream.height]) | ||
575 | expect(probe.format.bit_rate).to.be.at.least(minBitrateLimits[videoStream.height]) | ||
576 | |||
577 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
578 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
579 | } | ||
580 | } | ||
581 | }) | ||
582 | |||
583 | it('Should not generate an upper resolution than original file', async function () { | ||
584 | this.timeout(500_000) | ||
585 | |||
586 | const resolutions = [ 240, 480 ] | ||
587 | await updateConf(resolutions) | ||
588 | |||
589 | await servers[0].config.updateExistingSubConfig({ | ||
590 | newConfig: { | ||
591 | live: { | ||
592 | transcoding: { | ||
593 | alwaysTranscodeOriginalResolution: false | ||
594 | } | ||
595 | } | ||
596 | } | ||
597 | }) | ||
598 | |||
599 | liveVideoId = await createLiveWrapper(true) | ||
600 | |||
601 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
602 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
603 | await waitJobs(servers) | ||
604 | |||
605 | await testLiveVideoResolutions({ | ||
606 | originServer: servers[0], | ||
607 | sqlCommand: sqlCommandServer1, | ||
608 | servers, | ||
609 | liveVideoId, | ||
610 | resolutions, | ||
611 | transcoded: true | ||
612 | }) | ||
613 | |||
614 | await stopFfmpeg(ffmpegCommand) | ||
615 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
616 | |||
617 | await waitJobs(servers) | ||
618 | |||
619 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
620 | |||
621 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
622 | const hlsFiles = video.streamingPlaylists[0].files | ||
623 | |||
624 | expect(video.files).to.have.lengthOf(0) | ||
625 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | ||
626 | |||
627 | // eslint-disable-next-line @typescript-eslint/require-array-sort-compare | ||
628 | expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions) | ||
629 | }) | ||
630 | |||
631 | it('Should only keep the original resolution if all resolutions are disabled', async function () { | ||
632 | this.timeout(600_000) | ||
633 | |||
634 | await updateConf([]) | ||
635 | liveVideoId = await createLiveWrapper(true) | ||
636 | |||
637 | const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
638 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
639 | await waitJobs(servers) | ||
640 | |||
641 | await testLiveVideoResolutions({ | ||
642 | originServer: servers[0], | ||
643 | sqlCommand: sqlCommandServer1, | ||
644 | servers, | ||
645 | liveVideoId, | ||
646 | resolutions: [ 720 ], | ||
647 | transcoded: true | ||
648 | }) | ||
649 | |||
650 | await stopFfmpeg(ffmpegCommand) | ||
651 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
652 | |||
653 | await waitJobs(servers) | ||
654 | |||
655 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
656 | |||
657 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
658 | const hlsFiles = video.streamingPlaylists[0].files | ||
659 | |||
660 | expect(video.files).to.have.lengthOf(0) | ||
661 | expect(hlsFiles).to.have.lengthOf(1) | ||
662 | |||
663 | expect(hlsFiles[0].resolution.id).to.equal(720) | ||
664 | }) | ||
665 | |||
666 | after(async function () { | ||
667 | await sqlCommandServer1.cleanup() | ||
668 | }) | ||
669 | }) | ||
670 | |||
671 | describe('After a server restart', function () { | ||
672 | let liveVideoId: string | ||
673 | let liveVideoReplayId: string | ||
674 | let permanentLiveVideoReplayId: string | ||
675 | |||
676 | let permanentLiveReplayName: string | ||
677 | |||
678 | let beforeServerRestart: Date | ||
679 | |||
680 | async function createLiveWrapper (options: { saveReplay: boolean, permanent: boolean }) { | ||
681 | const liveAttributes: LiveVideoCreate = { | ||
682 | name: 'live video', | ||
683 | channelId: servers[0].store.channel.id, | ||
684 | privacy: VideoPrivacy.PUBLIC, | ||
685 | saveReplay: options.saveReplay, | ||
686 | replaySettings: options.saveReplay | ||
687 | ? { privacy: VideoPrivacy.PUBLIC } | ||
688 | : undefined, | ||
689 | permanentLive: options.permanent | ||
690 | } | ||
691 | |||
692 | const { uuid } = await commands[0].create({ fields: liveAttributes }) | ||
693 | return uuid | ||
694 | } | ||
695 | |||
696 | before(async function () { | ||
697 | this.timeout(600_000) | ||
698 | |||
699 | liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false }) | ||
700 | liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false }) | ||
701 | permanentLiveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: true }) | ||
702 | |||
703 | await Promise.all([ | ||
704 | commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }), | ||
705 | commands[0].sendRTMPStreamInVideo({ videoId: permanentLiveVideoReplayId }), | ||
706 | commands[0].sendRTMPStreamInVideo({ videoId: liveVideoReplayId }) | ||
707 | ]) | ||
708 | |||
709 | await Promise.all([ | ||
710 | commands[0].waitUntilPublished({ videoId: liveVideoId }), | ||
711 | commands[0].waitUntilPublished({ videoId: permanentLiveVideoReplayId }), | ||
712 | commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) | ||
713 | ]) | ||
714 | |||
715 | for (const videoUUID of [ liveVideoId, liveVideoReplayId, permanentLiveVideoReplayId ]) { | ||
716 | await commands[0].waitUntilSegmentGeneration({ | ||
717 | server: servers[0], | ||
718 | videoUUID, | ||
719 | playlistNumber: 0, | ||
720 | segment: 2 | ||
721 | }) | ||
722 | } | ||
723 | |||
724 | { | ||
725 | const video = await servers[0].videos.get({ id: permanentLiveVideoReplayId }) | ||
726 | permanentLiveReplayName = video.name + ' - ' + new Date(video.publishedAt).toLocaleString() | ||
727 | } | ||
728 | |||
729 | await killallServers([ servers[0] ]) | ||
730 | |||
731 | beforeServerRestart = new Date() | ||
732 | await servers[0].run() | ||
733 | |||
734 | await wait(5000) | ||
735 | await waitJobs(servers) | ||
736 | }) | ||
737 | |||
738 | it('Should cleanup lives', async function () { | ||
739 | this.timeout(60000) | ||
740 | |||
741 | await commands[0].waitUntilEnded({ videoId: liveVideoId }) | ||
742 | await commands[0].waitUntilWaiting({ videoId: permanentLiveVideoReplayId }) | ||
743 | }) | ||
744 | |||
745 | it('Should save a non permanent live replay', async function () { | ||
746 | this.timeout(240000) | ||
747 | |||
748 | await commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) | ||
749 | |||
750 | const session = await commands[0].getReplaySession({ videoId: liveVideoReplayId }) | ||
751 | expect(session.endDate).to.exist | ||
752 | expect(new Date(session.endDate)).to.be.above(beforeServerRestart) | ||
753 | }) | ||
754 | |||
755 | it('Should have saved a permanent live replay', async function () { | ||
756 | this.timeout(120000) | ||
757 | |||
758 | const { data } = await servers[0].videos.listMyVideos({ sort: '-publishedAt' }) | ||
759 | expect(data.find(v => v.name === permanentLiveReplayName)).to.exist | ||
760 | }) | ||
761 | }) | ||
762 | |||
763 | after(async function () { | ||
764 | await cleanupTests(servers) | ||
765 | }) | ||
766 | }) | ||
diff --git a/packages/tests/src/api/moderation/abuses.ts b/packages/tests/src/api/moderation/abuses.ts new file mode 100644 index 000000000..649de224e --- /dev/null +++ b/packages/tests/src/api/moderation/abuses.ts | |||
@@ -0,0 +1,887 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | AbusesCommand, | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test abuses', function () { | ||
18 | let servers: PeerTubeServer[] = [] | ||
19 | let abuseServer1: AdminAbuse | ||
20 | let abuseServer2: AdminAbuse | ||
21 | let commands: AbusesCommand[] | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(50000) | ||
25 | |||
26 | // Run servers | ||
27 | servers = await createMultipleServers(2) | ||
28 | |||
29 | await setAccessTokensToServers(servers) | ||
30 | await setDefaultChannelAvatar(servers) | ||
31 | await setDefaultAccountAvatar(servers) | ||
32 | |||
33 | // Server 1 and server 2 follow each other | ||
34 | await doubleFollow(servers[0], servers[1]) | ||
35 | |||
36 | commands = servers.map(s => s.abuses) | ||
37 | }) | ||
38 | |||
39 | describe('Video abuses', function () { | ||
40 | |||
41 | before(async function () { | ||
42 | this.timeout(50000) | ||
43 | |||
44 | // Upload some videos on each servers | ||
45 | { | ||
46 | const attributes = { | ||
47 | name: 'my super name for server 1', | ||
48 | description: 'my super description for server 1' | ||
49 | } | ||
50 | await servers[0].videos.upload({ attributes }) | ||
51 | } | ||
52 | |||
53 | { | ||
54 | const attributes = { | ||
55 | name: 'my super name for server 2', | ||
56 | description: 'my super description for server 2' | ||
57 | } | ||
58 | await servers[1].videos.upload({ attributes }) | ||
59 | } | ||
60 | |||
61 | // Wait videos propagation, server 2 has transcoding enabled | ||
62 | await waitJobs(servers) | ||
63 | |||
64 | const { data } = await servers[0].videos.list() | ||
65 | expect(data.length).to.equal(2) | ||
66 | |||
67 | servers[0].store.videoCreated = data.find(video => video.name === 'my super name for server 1') | ||
68 | servers[1].store.videoCreated = data.find(video => video.name === 'my super name for server 2') | ||
69 | }) | ||
70 | |||
71 | it('Should not have abuses', async function () { | ||
72 | const body = await commands[0].getAdminList() | ||
73 | |||
74 | expect(body.total).to.equal(0) | ||
75 | expect(body.data).to.be.an('array') | ||
76 | expect(body.data.length).to.equal(0) | ||
77 | }) | ||
78 | |||
79 | it('Should report abuse on a local video', async function () { | ||
80 | this.timeout(15000) | ||
81 | |||
82 | const reason = 'my super bad reason' | ||
83 | await commands[0].report({ videoId: servers[0].store.videoCreated.id, reason }) | ||
84 | |||
85 | // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2 | ||
86 | await waitJobs(servers) | ||
87 | }) | ||
88 | |||
89 | it('Should have 1 video abuses on server 1 and 0 on server 2', async function () { | ||
90 | { | ||
91 | const body = await commands[0].getAdminList() | ||
92 | |||
93 | expect(body.total).to.equal(1) | ||
94 | expect(body.data).to.be.an('array') | ||
95 | expect(body.data.length).to.equal(1) | ||
96 | |||
97 | const abuse = body.data[0] | ||
98 | expect(abuse.reason).to.equal('my super bad reason') | ||
99 | |||
100 | expect(abuse.reporterAccount.name).to.equal('root') | ||
101 | expect(abuse.reporterAccount.host).to.equal(servers[0].host) | ||
102 | |||
103 | expect(abuse.video.id).to.equal(servers[0].store.videoCreated.id) | ||
104 | expect(abuse.video.channel).to.exist | ||
105 | |||
106 | expect(abuse.comment).to.be.null | ||
107 | |||
108 | expect(abuse.flaggedAccount.name).to.equal('root') | ||
109 | expect(abuse.flaggedAccount.host).to.equal(servers[0].host) | ||
110 | |||
111 | expect(abuse.video.countReports).to.equal(1) | ||
112 | expect(abuse.video.nthReport).to.equal(1) | ||
113 | |||
114 | expect(abuse.countReportsForReporter).to.equal(1) | ||
115 | expect(abuse.countReportsForReportee).to.equal(1) | ||
116 | } | ||
117 | |||
118 | { | ||
119 | const body = await commands[1].getAdminList() | ||
120 | expect(body.total).to.equal(0) | ||
121 | expect(body.data).to.be.an('array') | ||
122 | expect(body.data.length).to.equal(0) | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | it('Should report abuse on a remote video', async function () { | ||
127 | const reason = 'my super bad reason 2' | ||
128 | const videoId = await servers[0].videos.getId({ uuid: servers[1].store.videoCreated.uuid }) | ||
129 | await commands[0].report({ videoId, reason }) | ||
130 | |||
131 | // We wait requests propagation | ||
132 | await waitJobs(servers) | ||
133 | }) | ||
134 | |||
135 | it('Should have 2 video abuses on server 1 and 1 on server 2', async function () { | ||
136 | { | ||
137 | const body = await commands[0].getAdminList() | ||
138 | |||
139 | expect(body.total).to.equal(2) | ||
140 | expect(body.data.length).to.equal(2) | ||
141 | |||
142 | const abuse1 = body.data[0] | ||
143 | expect(abuse1.reason).to.equal('my super bad reason') | ||
144 | expect(abuse1.reporterAccount.name).to.equal('root') | ||
145 | expect(abuse1.reporterAccount.host).to.equal(servers[0].host) | ||
146 | |||
147 | expect(abuse1.video.id).to.equal(servers[0].store.videoCreated.id) | ||
148 | expect(abuse1.video.countReports).to.equal(1) | ||
149 | expect(abuse1.video.nthReport).to.equal(1) | ||
150 | |||
151 | expect(abuse1.comment).to.be.null | ||
152 | |||
153 | expect(abuse1.flaggedAccount.name).to.equal('root') | ||
154 | expect(abuse1.flaggedAccount.host).to.equal(servers[0].host) | ||
155 | |||
156 | expect(abuse1.state.id).to.equal(AbuseState.PENDING) | ||
157 | expect(abuse1.state.label).to.equal('Pending') | ||
158 | expect(abuse1.moderationComment).to.be.null | ||
159 | |||
160 | const abuse2 = body.data[1] | ||
161 | expect(abuse2.reason).to.equal('my super bad reason 2') | ||
162 | |||
163 | expect(abuse2.reporterAccount.name).to.equal('root') | ||
164 | expect(abuse2.reporterAccount.host).to.equal(servers[0].host) | ||
165 | |||
166 | expect(abuse2.video.uuid).to.equal(servers[1].store.videoCreated.uuid) | ||
167 | |||
168 | expect(abuse2.comment).to.be.null | ||
169 | |||
170 | expect(abuse2.flaggedAccount.name).to.equal('root') | ||
171 | expect(abuse2.flaggedAccount.host).to.equal(servers[1].host) | ||
172 | |||
173 | expect(abuse2.state.id).to.equal(AbuseState.PENDING) | ||
174 | expect(abuse2.state.label).to.equal('Pending') | ||
175 | expect(abuse2.moderationComment).to.be.null | ||
176 | } | ||
177 | |||
178 | { | ||
179 | const body = await commands[1].getAdminList() | ||
180 | expect(body.total).to.equal(1) | ||
181 | expect(body.data.length).to.equal(1) | ||
182 | |||
183 | abuseServer2 = body.data[0] | ||
184 | expect(abuseServer2.reason).to.equal('my super bad reason 2') | ||
185 | expect(abuseServer2.reporterAccount.name).to.equal('root') | ||
186 | expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) | ||
187 | |||
188 | expect(abuseServer2.flaggedAccount.name).to.equal('root') | ||
189 | expect(abuseServer2.flaggedAccount.host).to.equal(servers[1].host) | ||
190 | |||
191 | expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) | ||
192 | expect(abuseServer2.state.label).to.equal('Pending') | ||
193 | expect(abuseServer2.moderationComment).to.be.null | ||
194 | } | ||
195 | }) | ||
196 | |||
197 | it('Should hide video abuses from blocked accounts', async function () { | ||
198 | { | ||
199 | const videoId = await servers[1].videos.getId({ uuid: servers[0].store.videoCreated.uuid }) | ||
200 | await commands[1].report({ videoId, reason: 'will mute this' }) | ||
201 | await waitJobs(servers) | ||
202 | |||
203 | const body = await commands[0].getAdminList() | ||
204 | expect(body.total).to.equal(3) | ||
205 | } | ||
206 | |||
207 | const accountToBlock = 'root@' + servers[1].host | ||
208 | |||
209 | { | ||
210 | await servers[0].blocklist.addToServerBlocklist({ account: accountToBlock }) | ||
211 | |||
212 | const body = await commands[0].getAdminList() | ||
213 | expect(body.total).to.equal(2) | ||
214 | |||
215 | const abuse = body.data.find(a => a.reason === 'will mute this') | ||
216 | expect(abuse).to.be.undefined | ||
217 | } | ||
218 | |||
219 | { | ||
220 | await servers[0].blocklist.removeFromServerBlocklist({ account: accountToBlock }) | ||
221 | |||
222 | const body = await commands[0].getAdminList() | ||
223 | expect(body.total).to.equal(3) | ||
224 | } | ||
225 | }) | ||
226 | |||
227 | it('Should hide video abuses from blocked servers', async function () { | ||
228 | const serverToBlock = servers[1].host | ||
229 | |||
230 | { | ||
231 | await servers[0].blocklist.addToServerBlocklist({ server: serverToBlock }) | ||
232 | |||
233 | const body = await commands[0].getAdminList() | ||
234 | expect(body.total).to.equal(2) | ||
235 | |||
236 | const abuse = body.data.find(a => a.reason === 'will mute this') | ||
237 | expect(abuse).to.be.undefined | ||
238 | } | ||
239 | |||
240 | { | ||
241 | await servers[0].blocklist.removeFromServerBlocklist({ server: serverToBlock }) | ||
242 | |||
243 | const body = await commands[0].getAdminList() | ||
244 | expect(body.total).to.equal(3) | ||
245 | } | ||
246 | }) | ||
247 | |||
248 | it('Should keep the video abuse when deleting the video', async function () { | ||
249 | await servers[1].videos.remove({ id: abuseServer2.video.uuid }) | ||
250 | |||
251 | await waitJobs(servers) | ||
252 | |||
253 | const body = await commands[1].getAdminList() | ||
254 | expect(body.total).to.equal(2, 'wrong number of videos returned') | ||
255 | expect(body.data).to.have.lengthOf(2, 'wrong number of videos returned') | ||
256 | |||
257 | const abuse = body.data[0] | ||
258 | expect(abuse.id).to.equal(abuseServer2.id, 'wrong origin server id for first video') | ||
259 | expect(abuse.video.id).to.equal(abuseServer2.video.id, 'wrong video id') | ||
260 | expect(abuse.video.channel).to.exist | ||
261 | expect(abuse.video.deleted).to.be.true | ||
262 | }) | ||
263 | |||
264 | it('Should include counts of reports from reporter and reportee', async function () { | ||
265 | // register a second user to have two reporters/reportees | ||
266 | const user = { username: 'user2', password: 'password' } | ||
267 | await servers[0].users.create({ ...user }) | ||
268 | const userAccessToken = await servers[0].login.getAccessToken(user) | ||
269 | |||
270 | // upload a third video via this user | ||
271 | const attributes = { | ||
272 | name: 'my second super name for server 1', | ||
273 | description: 'my second super description for server 1' | ||
274 | } | ||
275 | const { id } = await servers[0].videos.upload({ token: userAccessToken, attributes }) | ||
276 | const video3Id = id | ||
277 | |||
278 | // resume with the test | ||
279 | const reason3 = 'my super bad reason 3' | ||
280 | await commands[0].report({ videoId: video3Id, reason: reason3 }) | ||
281 | |||
282 | const reason4 = 'my super bad reason 4' | ||
283 | await commands[0].report({ token: userAccessToken, videoId: servers[0].store.videoCreated.id, reason: reason4 }) | ||
284 | |||
285 | { | ||
286 | const body = await commands[0].getAdminList() | ||
287 | const abuses = body.data | ||
288 | |||
289 | const abuseVideo3 = body.data.find(a => a.video.id === video3Id) | ||
290 | expect(abuseVideo3).to.not.be.undefined | ||
291 | expect(abuseVideo3.video.countReports).to.equal(1, 'wrong reports count for video 3') | ||
292 | expect(abuseVideo3.video.nthReport).to.equal(1, 'wrong report position in report list for video 3') | ||
293 | expect(abuseVideo3.countReportsForReportee).to.equal(1, 'wrong reports count for reporter on video 3 abuse') | ||
294 | expect(abuseVideo3.countReportsForReporter).to.equal(3, 'wrong reports count for reportee on video 3 abuse') | ||
295 | |||
296 | const abuseServer1 = abuses.find(a => a.video.id === servers[0].store.videoCreated.id) | ||
297 | expect(abuseServer1.countReportsForReportee).to.equal(3, 'wrong reports count for reporter on video 1 abuse') | ||
298 | } | ||
299 | }) | ||
300 | |||
301 | it('Should list predefined reasons as well as timestamps for the reported video', async function () { | ||
302 | const reason5 = 'my super bad reason 5' | ||
303 | const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] | ||
304 | const createRes = await commands[0].report({ | ||
305 | videoId: servers[0].store.videoCreated.id, | ||
306 | reason: reason5, | ||
307 | predefinedReasons: predefinedReasons5, | ||
308 | startAt: 1, | ||
309 | endAt: 5 | ||
310 | }) | ||
311 | |||
312 | const body = await commands[0].getAdminList() | ||
313 | |||
314 | { | ||
315 | const abuse = body.data.find(a => a.id === createRes.abuse.id) | ||
316 | expect(abuse.reason).to.equals(reason5) | ||
317 | expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, 'predefined reasons do not match the one reported') | ||
318 | expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported") | ||
319 | expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported") | ||
320 | } | ||
321 | }) | ||
322 | |||
323 | it('Should delete the video abuse', async function () { | ||
324 | await commands[1].delete({ abuseId: abuseServer2.id }) | ||
325 | |||
326 | await waitJobs(servers) | ||
327 | |||
328 | { | ||
329 | const body = await commands[1].getAdminList() | ||
330 | expect(body.total).to.equal(1) | ||
331 | expect(body.data.length).to.equal(1) | ||
332 | expect(body.data[0].id).to.not.equal(abuseServer2.id) | ||
333 | } | ||
334 | |||
335 | { | ||
336 | const body = await commands[0].getAdminList() | ||
337 | expect(body.total).to.equal(6) | ||
338 | } | ||
339 | }) | ||
340 | |||
341 | it('Should list and filter video abuses', async function () { | ||
342 | async function list (query: Parameters<AbusesCommand['getAdminList']>[0]) { | ||
343 | const body = await commands[0].getAdminList(query) | ||
344 | |||
345 | return body.data | ||
346 | } | ||
347 | |||
348 | expect(await list({ id: 56 })).to.have.lengthOf(0) | ||
349 | expect(await list({ id: 1 })).to.have.lengthOf(1) | ||
350 | |||
351 | expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4) | ||
352 | expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0) | ||
353 | |||
354 | expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1) | ||
355 | |||
356 | expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4) | ||
357 | expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0) | ||
358 | |||
359 | expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) | ||
360 | expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5) | ||
361 | |||
362 | expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5) | ||
363 | expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) | ||
364 | |||
365 | expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) | ||
366 | expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) | ||
367 | |||
368 | expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0) | ||
369 | expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6) | ||
370 | |||
371 | expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) | ||
372 | expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0) | ||
373 | }) | ||
374 | }) | ||
375 | |||
376 | describe('Comment abuses', function () { | ||
377 | |||
378 | async function getComment (server: PeerTubeServer, videoIdArg: number | string) { | ||
379 | const videoId = typeof videoIdArg === 'string' | ||
380 | ? await server.videos.getId({ uuid: videoIdArg }) | ||
381 | : videoIdArg | ||
382 | |||
383 | const { data } = await server.comments.listThreads({ videoId }) | ||
384 | |||
385 | return data[0] | ||
386 | } | ||
387 | |||
388 | before(async function () { | ||
389 | this.timeout(50000) | ||
390 | |||
391 | servers[0].store.videoCreated = await servers[0].videos.quickUpload({ name: 'server 1' }) | ||
392 | servers[1].store.videoCreated = await servers[1].videos.quickUpload({ name: 'server 2' }) | ||
393 | |||
394 | await servers[0].comments.createThread({ videoId: servers[0].store.videoCreated.id, text: 'comment server 1' }) | ||
395 | await servers[1].comments.createThread({ videoId: servers[1].store.videoCreated.id, text: 'comment server 2' }) | ||
396 | |||
397 | await waitJobs(servers) | ||
398 | }) | ||
399 | |||
400 | it('Should report abuse on a comment', async function () { | ||
401 | this.timeout(15000) | ||
402 | |||
403 | const comment = await getComment(servers[0], servers[0].store.videoCreated.id) | ||
404 | |||
405 | const reason = 'it is a bad comment' | ||
406 | await commands[0].report({ commentId: comment.id, reason }) | ||
407 | |||
408 | await waitJobs(servers) | ||
409 | }) | ||
410 | |||
411 | it('Should have 1 comment abuse on server 1 and 0 on server 2', async function () { | ||
412 | { | ||
413 | const comment = await getComment(servers[0], servers[0].store.videoCreated.id) | ||
414 | const body = await commands[0].getAdminList({ filter: 'comment' }) | ||
415 | |||
416 | expect(body.total).to.equal(1) | ||
417 | expect(body.data).to.have.lengthOf(1) | ||
418 | |||
419 | const abuse = body.data[0] | ||
420 | expect(abuse.reason).to.equal('it is a bad comment') | ||
421 | |||
422 | expect(abuse.reporterAccount.name).to.equal('root') | ||
423 | expect(abuse.reporterAccount.host).to.equal(servers[0].host) | ||
424 | |||
425 | expect(abuse.video).to.be.null | ||
426 | |||
427 | expect(abuse.comment.deleted).to.be.false | ||
428 | expect(abuse.comment.id).to.equal(comment.id) | ||
429 | expect(abuse.comment.text).to.equal(comment.text) | ||
430 | expect(abuse.comment.video.name).to.equal('server 1') | ||
431 | expect(abuse.comment.video.id).to.equal(servers[0].store.videoCreated.id) | ||
432 | expect(abuse.comment.video.uuid).to.equal(servers[0].store.videoCreated.uuid) | ||
433 | |||
434 | expect(abuse.countReportsForReporter).to.equal(5) | ||
435 | expect(abuse.countReportsForReportee).to.equal(5) | ||
436 | } | ||
437 | |||
438 | { | ||
439 | const body = await commands[1].getAdminList({ filter: 'comment' }) | ||
440 | expect(body.total).to.equal(0) | ||
441 | expect(body.data.length).to.equal(0) | ||
442 | } | ||
443 | }) | ||
444 | |||
445 | it('Should report abuse on a remote comment', async function () { | ||
446 | const comment = await getComment(servers[0], servers[1].store.videoCreated.uuid) | ||
447 | |||
448 | const reason = 'it is a really bad comment' | ||
449 | await commands[0].report({ commentId: comment.id, reason }) | ||
450 | |||
451 | await waitJobs(servers) | ||
452 | }) | ||
453 | |||
454 | it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () { | ||
455 | const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.shortUUID) | ||
456 | |||
457 | { | ||
458 | const body = await commands[0].getAdminList({ filter: 'comment' }) | ||
459 | expect(body.total).to.equal(2) | ||
460 | expect(body.data.length).to.equal(2) | ||
461 | |||
462 | const abuse = body.data[0] | ||
463 | expect(abuse.reason).to.equal('it is a bad comment') | ||
464 | expect(abuse.countReportsForReporter).to.equal(6) | ||
465 | expect(abuse.countReportsForReportee).to.equal(5) | ||
466 | |||
467 | const abuse2 = body.data[1] | ||
468 | |||
469 | expect(abuse2.reason).to.equal('it is a really bad comment') | ||
470 | |||
471 | expect(abuse2.reporterAccount.name).to.equal('root') | ||
472 | expect(abuse2.reporterAccount.host).to.equal(servers[0].host) | ||
473 | |||
474 | expect(abuse2.video).to.be.null | ||
475 | |||
476 | expect(abuse2.comment.deleted).to.be.false | ||
477 | expect(abuse2.comment.id).to.equal(commentServer2.id) | ||
478 | expect(abuse2.comment.text).to.equal(commentServer2.text) | ||
479 | expect(abuse2.comment.video.name).to.equal('server 2') | ||
480 | expect(abuse2.comment.video.uuid).to.equal(servers[1].store.videoCreated.uuid) | ||
481 | |||
482 | expect(abuse2.state.id).to.equal(AbuseState.PENDING) | ||
483 | expect(abuse2.state.label).to.equal('Pending') | ||
484 | |||
485 | expect(abuse2.moderationComment).to.be.null | ||
486 | |||
487 | expect(abuse2.countReportsForReporter).to.equal(6) | ||
488 | expect(abuse2.countReportsForReportee).to.equal(2) | ||
489 | } | ||
490 | |||
491 | { | ||
492 | const body = await commands[1].getAdminList({ filter: 'comment' }) | ||
493 | expect(body.total).to.equal(1) | ||
494 | expect(body.data.length).to.equal(1) | ||
495 | |||
496 | abuseServer2 = body.data[0] | ||
497 | expect(abuseServer2.reason).to.equal('it is a really bad comment') | ||
498 | expect(abuseServer2.reporterAccount.name).to.equal('root') | ||
499 | expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) | ||
500 | |||
501 | expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) | ||
502 | expect(abuseServer2.state.label).to.equal('Pending') | ||
503 | |||
504 | expect(abuseServer2.moderationComment).to.be.null | ||
505 | |||
506 | expect(abuseServer2.countReportsForReporter).to.equal(1) | ||
507 | expect(abuseServer2.countReportsForReportee).to.equal(1) | ||
508 | } | ||
509 | }) | ||
510 | |||
511 | it('Should keep the comment abuse when deleting the comment', async function () { | ||
512 | const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.uuid) | ||
513 | |||
514 | await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id }) | ||
515 | |||
516 | await waitJobs(servers) | ||
517 | |||
518 | const body = await commands[0].getAdminList({ filter: 'comment' }) | ||
519 | expect(body.total).to.equal(2) | ||
520 | expect(body.data).to.have.lengthOf(2) | ||
521 | |||
522 | const abuse = body.data.find(a => a.comment?.id === commentServer2.id) | ||
523 | expect(abuse).to.not.be.undefined | ||
524 | |||
525 | expect(abuse.comment.text).to.be.empty | ||
526 | expect(abuse.comment.video.name).to.equal('server 2') | ||
527 | expect(abuse.comment.deleted).to.be.true | ||
528 | }) | ||
529 | |||
530 | it('Should delete the comment abuse', async function () { | ||
531 | await commands[1].delete({ abuseId: abuseServer2.id }) | ||
532 | |||
533 | await waitJobs(servers) | ||
534 | |||
535 | { | ||
536 | const body = await commands[1].getAdminList({ filter: 'comment' }) | ||
537 | expect(body.total).to.equal(0) | ||
538 | expect(body.data.length).to.equal(0) | ||
539 | } | ||
540 | |||
541 | { | ||
542 | const body = await commands[0].getAdminList({ filter: 'comment' }) | ||
543 | expect(body.total).to.equal(2) | ||
544 | } | ||
545 | }) | ||
546 | |||
547 | it('Should list and filter video abuses', async function () { | ||
548 | { | ||
549 | const body = await commands[0].getAdminList({ filter: 'comment', searchReportee: 'foo' }) | ||
550 | expect(body.total).to.equal(0) | ||
551 | } | ||
552 | |||
553 | { | ||
554 | const body = await commands[0].getAdminList({ filter: 'comment', searchReportee: 'ot' }) | ||
555 | expect(body.total).to.equal(2) | ||
556 | } | ||
557 | |||
558 | { | ||
559 | const body = await commands[0].getAdminList({ filter: 'comment', start: 1, count: 1, sort: 'createdAt' }) | ||
560 | expect(body.data).to.have.lengthOf(1) | ||
561 | expect(body.data[0].comment.text).to.be.empty | ||
562 | } | ||
563 | |||
564 | { | ||
565 | const body = await commands[0].getAdminList({ filter: 'comment', start: 1, count: 1, sort: '-createdAt' }) | ||
566 | expect(body.data).to.have.lengthOf(1) | ||
567 | expect(body.data[0].comment.text).to.equal('comment server 1') | ||
568 | } | ||
569 | }) | ||
570 | }) | ||
571 | |||
572 | describe('Account abuses', function () { | ||
573 | |||
574 | function getAccountFromServer (server: PeerTubeServer, targetName: string, targetServer: PeerTubeServer) { | ||
575 | return server.accounts.get({ accountName: targetName + '@' + targetServer.host }) | ||
576 | } | ||
577 | |||
578 | before(async function () { | ||
579 | this.timeout(50000) | ||
580 | |||
581 | await servers[0].users.create({ username: 'user_1', password: 'donald' }) | ||
582 | |||
583 | const token = await servers[1].users.generateUserAndToken('user_2') | ||
584 | await servers[1].videos.upload({ token, attributes: { name: 'super video' } }) | ||
585 | |||
586 | await waitJobs(servers) | ||
587 | }) | ||
588 | |||
589 | it('Should report abuse on an account', async function () { | ||
590 | this.timeout(15000) | ||
591 | |||
592 | const account = await getAccountFromServer(servers[0], 'user_1', servers[0]) | ||
593 | |||
594 | const reason = 'it is a bad account' | ||
595 | await commands[0].report({ accountId: account.id, reason }) | ||
596 | |||
597 | await waitJobs(servers) | ||
598 | }) | ||
599 | |||
600 | it('Should have 1 account abuse on server 1 and 0 on server 2', async function () { | ||
601 | { | ||
602 | const body = await commands[0].getAdminList({ filter: 'account' }) | ||
603 | |||
604 | expect(body.total).to.equal(1) | ||
605 | expect(body.data).to.have.lengthOf(1) | ||
606 | |||
607 | const abuse = body.data[0] | ||
608 | expect(abuse.reason).to.equal('it is a bad account') | ||
609 | |||
610 | expect(abuse.reporterAccount.name).to.equal('root') | ||
611 | expect(abuse.reporterAccount.host).to.equal(servers[0].host) | ||
612 | |||
613 | expect(abuse.video).to.be.null | ||
614 | expect(abuse.comment).to.be.null | ||
615 | |||
616 | expect(abuse.flaggedAccount.name).to.equal('user_1') | ||
617 | expect(abuse.flaggedAccount.host).to.equal(servers[0].host) | ||
618 | } | ||
619 | |||
620 | { | ||
621 | const body = await commands[1].getAdminList({ filter: 'comment' }) | ||
622 | expect(body.total).to.equal(0) | ||
623 | expect(body.data.length).to.equal(0) | ||
624 | } | ||
625 | }) | ||
626 | |||
627 | it('Should report abuse on a remote account', async function () { | ||
628 | const account = await getAccountFromServer(servers[0], 'user_2', servers[1]) | ||
629 | |||
630 | const reason = 'it is a really bad account' | ||
631 | await commands[0].report({ accountId: account.id, reason }) | ||
632 | |||
633 | await waitJobs(servers) | ||
634 | }) | ||
635 | |||
636 | it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () { | ||
637 | { | ||
638 | const body = await commands[0].getAdminList({ filter: 'account' }) | ||
639 | expect(body.total).to.equal(2) | ||
640 | expect(body.data.length).to.equal(2) | ||
641 | |||
642 | const abuse: AdminAbuse = body.data[0] | ||
643 | expect(abuse.reason).to.equal('it is a bad account') | ||
644 | |||
645 | const abuse2: AdminAbuse = body.data[1] | ||
646 | expect(abuse2.reason).to.equal('it is a really bad account') | ||
647 | |||
648 | expect(abuse2.reporterAccount.name).to.equal('root') | ||
649 | expect(abuse2.reporterAccount.host).to.equal(servers[0].host) | ||
650 | |||
651 | expect(abuse2.video).to.be.null | ||
652 | expect(abuse2.comment).to.be.null | ||
653 | |||
654 | expect(abuse2.state.id).to.equal(AbuseState.PENDING) | ||
655 | expect(abuse2.state.label).to.equal('Pending') | ||
656 | |||
657 | expect(abuse2.moderationComment).to.be.null | ||
658 | } | ||
659 | |||
660 | { | ||
661 | const body = await commands[1].getAdminList({ filter: 'account' }) | ||
662 | expect(body.total).to.equal(1) | ||
663 | expect(body.data.length).to.equal(1) | ||
664 | |||
665 | abuseServer2 = body.data[0] | ||
666 | |||
667 | expect(abuseServer2.reason).to.equal('it is a really bad account') | ||
668 | |||
669 | expect(abuseServer2.reporterAccount.name).to.equal('root') | ||
670 | expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host) | ||
671 | |||
672 | expect(abuseServer2.state.id).to.equal(AbuseState.PENDING) | ||
673 | expect(abuseServer2.state.label).to.equal('Pending') | ||
674 | |||
675 | expect(abuseServer2.moderationComment).to.be.null | ||
676 | } | ||
677 | }) | ||
678 | |||
679 | it('Should keep the account abuse when deleting the account', async function () { | ||
680 | const account = await getAccountFromServer(servers[1], 'user_2', servers[1]) | ||
681 | await servers[1].users.remove({ userId: account.userId }) | ||
682 | |||
683 | await waitJobs(servers) | ||
684 | |||
685 | const body = await commands[0].getAdminList({ filter: 'account' }) | ||
686 | expect(body.total).to.equal(2) | ||
687 | expect(body.data).to.have.lengthOf(2) | ||
688 | |||
689 | const abuse = body.data.find(a => a.reason === 'it is a really bad account') | ||
690 | expect(abuse).to.not.be.undefined | ||
691 | }) | ||
692 | |||
693 | it('Should delete the account abuse', async function () { | ||
694 | await commands[1].delete({ abuseId: abuseServer2.id }) | ||
695 | |||
696 | await waitJobs(servers) | ||
697 | |||
698 | { | ||
699 | const body = await commands[1].getAdminList({ filter: 'account' }) | ||
700 | expect(body.total).to.equal(0) | ||
701 | expect(body.data.length).to.equal(0) | ||
702 | } | ||
703 | |||
704 | { | ||
705 | const body = await commands[0].getAdminList({ filter: 'account' }) | ||
706 | expect(body.total).to.equal(2) | ||
707 | |||
708 | abuseServer1 = body.data[0] | ||
709 | } | ||
710 | }) | ||
711 | }) | ||
712 | |||
713 | describe('Common actions on abuses', function () { | ||
714 | |||
715 | it('Should update the state of an abuse', async function () { | ||
716 | await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.REJECTED } }) | ||
717 | |||
718 | const body = await commands[0].getAdminList({ id: abuseServer1.id }) | ||
719 | expect(body.data[0].state.id).to.equal(AbuseState.REJECTED) | ||
720 | }) | ||
721 | |||
722 | it('Should add a moderation comment', async function () { | ||
723 | await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.ACCEPTED, moderationComment: 'Valid' } }) | ||
724 | |||
725 | const body = await commands[0].getAdminList({ id: abuseServer1.id }) | ||
726 | expect(body.data[0].state.id).to.equal(AbuseState.ACCEPTED) | ||
727 | expect(body.data[0].moderationComment).to.equal('Valid') | ||
728 | }) | ||
729 | }) | ||
730 | |||
731 | describe('My abuses', async function () { | ||
732 | let abuseId1: number | ||
733 | let userAccessToken: string | ||
734 | |||
735 | before(async function () { | ||
736 | userAccessToken = await servers[0].users.generateUserAndToken('user_42') | ||
737 | |||
738 | await commands[0].report({ token: userAccessToken, videoId: servers[0].store.videoCreated.id, reason: 'user reason 1' }) | ||
739 | |||
740 | const videoId = await servers[0].videos.getId({ uuid: servers[1].store.videoCreated.uuid }) | ||
741 | await commands[0].report({ token: userAccessToken, videoId, reason: 'user reason 2' }) | ||
742 | }) | ||
743 | |||
744 | it('Should correctly list my abuses', async function () { | ||
745 | { | ||
746 | const body = await commands[0].getUserList({ token: userAccessToken, start: 0, count: 5, sort: 'createdAt' }) | ||
747 | expect(body.total).to.equal(2) | ||
748 | |||
749 | const abuses = body.data | ||
750 | expect(abuses[0].reason).to.equal('user reason 1') | ||
751 | expect(abuses[1].reason).to.equal('user reason 2') | ||
752 | |||
753 | abuseId1 = abuses[0].id | ||
754 | } | ||
755 | |||
756 | { | ||
757 | const body = await commands[0].getUserList({ token: userAccessToken, start: 1, count: 1, sort: 'createdAt' }) | ||
758 | expect(body.total).to.equal(2) | ||
759 | |||
760 | const abuses: UserAbuse[] = body.data | ||
761 | expect(abuses[0].reason).to.equal('user reason 2') | ||
762 | } | ||
763 | |||
764 | { | ||
765 | const body = await commands[0].getUserList({ token: userAccessToken, start: 1, count: 1, sort: '-createdAt' }) | ||
766 | expect(body.total).to.equal(2) | ||
767 | |||
768 | const abuses: UserAbuse[] = body.data | ||
769 | expect(abuses[0].reason).to.equal('user reason 1') | ||
770 | } | ||
771 | }) | ||
772 | |||
773 | it('Should correctly filter my abuses by id', async function () { | ||
774 | const body = await commands[0].getUserList({ token: userAccessToken, id: abuseId1 }) | ||
775 | expect(body.total).to.equal(1) | ||
776 | |||
777 | const abuses: UserAbuse[] = body.data | ||
778 | expect(abuses[0].reason).to.equal('user reason 1') | ||
779 | }) | ||
780 | |||
781 | it('Should correctly filter my abuses by search', async function () { | ||
782 | const body = await commands[0].getUserList({ token: userAccessToken, search: 'server 2' }) | ||
783 | expect(body.total).to.equal(1) | ||
784 | |||
785 | const abuses: UserAbuse[] = body.data | ||
786 | expect(abuses[0].reason).to.equal('user reason 2') | ||
787 | }) | ||
788 | |||
789 | it('Should correctly filter my abuses by state', async function () { | ||
790 | await commands[0].update({ abuseId: abuseId1, body: { state: AbuseState.REJECTED } }) | ||
791 | |||
792 | const body = await commands[0].getUserList({ token: userAccessToken, state: AbuseState.REJECTED }) | ||
793 | expect(body.total).to.equal(1) | ||
794 | |||
795 | const abuses: UserAbuse[] = body.data | ||
796 | expect(abuses[0].reason).to.equal('user reason 1') | ||
797 | }) | ||
798 | }) | ||
799 | |||
800 | describe('Abuse messages', async function () { | ||
801 | let abuseId: number | ||
802 | let userToken: string | ||
803 | let abuseMessageUserId: number | ||
804 | let abuseMessageModerationId: number | ||
805 | |||
806 | before(async function () { | ||
807 | userToken = await servers[0].users.generateUserAndToken('user_43') | ||
808 | |||
809 | const body = await commands[0].report({ token: userToken, videoId: servers[0].store.videoCreated.id, reason: 'user 43 reason 1' }) | ||
810 | abuseId = body.abuse.id | ||
811 | }) | ||
812 | |||
813 | it('Should create some messages on the abuse', async function () { | ||
814 | await commands[0].addMessage({ token: userToken, abuseId, message: 'message 1' }) | ||
815 | await commands[0].addMessage({ abuseId, message: 'message 2' }) | ||
816 | await commands[0].addMessage({ abuseId, message: 'message 3' }) | ||
817 | await commands[0].addMessage({ token: userToken, abuseId, message: 'message 4' }) | ||
818 | }) | ||
819 | |||
820 | it('Should have the correct messages count when listing abuses', async function () { | ||
821 | const results = await Promise.all([ | ||
822 | commands[0].getAdminList({ start: 0, count: 50 }), | ||
823 | commands[0].getUserList({ token: userToken, start: 0, count: 50 }) | ||
824 | ]) | ||
825 | |||
826 | for (const body of results) { | ||
827 | const abuses = body.data | ||
828 | const abuse = abuses.find(a => a.id === abuseId) | ||
829 | expect(abuse.countMessages).to.equal(4) | ||
830 | } | ||
831 | }) | ||
832 | |||
833 | it('Should correctly list messages of this abuse', async function () { | ||
834 | const results = await Promise.all([ | ||
835 | commands[0].listMessages({ abuseId }), | ||
836 | commands[0].listMessages({ token: userToken, abuseId }) | ||
837 | ]) | ||
838 | |||
839 | for (const body of results) { | ||
840 | expect(body.total).to.equal(4) | ||
841 | |||
842 | const abuseMessages: AbuseMessage[] = body.data | ||
843 | |||
844 | expect(abuseMessages[0].message).to.equal('message 1') | ||
845 | expect(abuseMessages[0].byModerator).to.be.false | ||
846 | expect(abuseMessages[0].account.name).to.equal('user_43') | ||
847 | |||
848 | abuseMessageUserId = abuseMessages[0].id | ||
849 | |||
850 | expect(abuseMessages[1].message).to.equal('message 2') | ||
851 | expect(abuseMessages[1].byModerator).to.be.true | ||
852 | expect(abuseMessages[1].account.name).to.equal('root') | ||
853 | |||
854 | expect(abuseMessages[2].message).to.equal('message 3') | ||
855 | expect(abuseMessages[2].byModerator).to.be.true | ||
856 | expect(abuseMessages[2].account.name).to.equal('root') | ||
857 | abuseMessageModerationId = abuseMessages[2].id | ||
858 | |||
859 | expect(abuseMessages[3].message).to.equal('message 4') | ||
860 | expect(abuseMessages[3].byModerator).to.be.false | ||
861 | expect(abuseMessages[3].account.name).to.equal('user_43') | ||
862 | } | ||
863 | }) | ||
864 | |||
865 | it('Should delete messages', async function () { | ||
866 | await commands[0].deleteMessage({ abuseId, messageId: abuseMessageModerationId }) | ||
867 | await commands[0].deleteMessage({ token: userToken, abuseId, messageId: abuseMessageUserId }) | ||
868 | |||
869 | const results = await Promise.all([ | ||
870 | commands[0].listMessages({ abuseId }), | ||
871 | commands[0].listMessages({ token: userToken, abuseId }) | ||
872 | ]) | ||
873 | |||
874 | for (const body of results) { | ||
875 | expect(body.total).to.equal(2) | ||
876 | |||
877 | const abuseMessages: AbuseMessage[] = body.data | ||
878 | expect(abuseMessages[0].message).to.equal('message 2') | ||
879 | expect(abuseMessages[1].message).to.equal('message 4') | ||
880 | } | ||
881 | }) | ||
882 | }) | ||
883 | |||
884 | after(async function () { | ||
885 | await cleanupTests(servers) | ||
886 | }) | ||
887 | }) | ||
diff --git a/packages/tests/src/api/moderation/blocklist-notification.ts b/packages/tests/src/api/moderation/blocklist-notification.ts new file mode 100644 index 000000000..abf36313b --- /dev/null +++ b/packages/tests/src/api/moderation/blocklist-notification.ts | |||
@@ -0,0 +1,231 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { UserNotificationType, UserNotificationType_Type } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | async function checkNotifications (server: PeerTubeServer, token: string, expected: UserNotificationType_Type[]) { | ||
15 | const { data } = await server.notifications.list({ token, start: 0, count: 10, unread: true }) | ||
16 | expect(data).to.have.lengthOf(expected.length) | ||
17 | |||
18 | for (const type of expected) { | ||
19 | expect(data.find(n => n.type === type)).to.exist | ||
20 | } | ||
21 | } | ||
22 | |||
23 | describe('Test blocklist notifications', function () { | ||
24 | let servers: PeerTubeServer[] | ||
25 | let videoUUID: string | ||
26 | |||
27 | let userToken1: string | ||
28 | let userToken2: string | ||
29 | let remoteUserToken: string | ||
30 | |||
31 | async function resetState () { | ||
32 | try { | ||
33 | await servers[1].subscriptions.remove({ token: remoteUserToken, uri: 'user1_channel@' + servers[0].host }) | ||
34 | await servers[1].subscriptions.remove({ token: remoteUserToken, uri: 'user2_channel@' + servers[0].host }) | ||
35 | } catch {} | ||
36 | |||
37 | await waitJobs(servers) | ||
38 | |||
39 | await servers[0].notifications.markAsReadAll({ token: userToken1 }) | ||
40 | await servers[0].notifications.markAsReadAll({ token: userToken2 }) | ||
41 | |||
42 | { | ||
43 | const { uuid } = await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video' } }) | ||
44 | videoUUID = uuid | ||
45 | |||
46 | await waitJobs(servers) | ||
47 | } | ||
48 | |||
49 | { | ||
50 | await servers[1].comments.createThread({ | ||
51 | token: remoteUserToken, | ||
52 | videoId: videoUUID, | ||
53 | text: '@user2@' + servers[0].host + ' hello' | ||
54 | }) | ||
55 | } | ||
56 | |||
57 | { | ||
58 | |||
59 | await servers[1].subscriptions.add({ token: remoteUserToken, targetUri: 'user1_channel@' + servers[0].host }) | ||
60 | await servers[1].subscriptions.add({ token: remoteUserToken, targetUri: 'user2_channel@' + servers[0].host }) | ||
61 | } | ||
62 | |||
63 | await waitJobs(servers) | ||
64 | } | ||
65 | |||
66 | before(async function () { | ||
67 | this.timeout(60000) | ||
68 | |||
69 | servers = await createMultipleServers(2) | ||
70 | await setAccessTokensToServers(servers) | ||
71 | |||
72 | { | ||
73 | const user = { username: 'user1', password: 'password' } | ||
74 | await servers[0].users.create({ | ||
75 | username: user.username, | ||
76 | password: user.password, | ||
77 | videoQuota: -1, | ||
78 | videoQuotaDaily: -1 | ||
79 | }) | ||
80 | |||
81 | userToken1 = await servers[0].login.getAccessToken(user) | ||
82 | await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video user 1' } }) | ||
83 | } | ||
84 | |||
85 | { | ||
86 | const user = { username: 'user2', password: 'password' } | ||
87 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
88 | |||
89 | userToken2 = await servers[0].login.getAccessToken(user) | ||
90 | } | ||
91 | |||
92 | { | ||
93 | const user = { username: 'user3', password: 'password' } | ||
94 | await servers[1].users.create({ username: user.username, password: user.password }) | ||
95 | |||
96 | remoteUserToken = await servers[1].login.getAccessToken(user) | ||
97 | } | ||
98 | |||
99 | await doubleFollow(servers[0], servers[1]) | ||
100 | }) | ||
101 | |||
102 | describe('User blocks another user', function () { | ||
103 | |||
104 | before(async function () { | ||
105 | this.timeout(30000) | ||
106 | |||
107 | await resetState() | ||
108 | }) | ||
109 | |||
110 | it('Should have appropriate notifications', async function () { | ||
111 | const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ] | ||
112 | await checkNotifications(servers[0], userToken1, notifs) | ||
113 | }) | ||
114 | |||
115 | it('Should block an account', async function () { | ||
116 | await servers[0].blocklist.addToMyBlocklist({ token: userToken1, account: 'user3@' + servers[1].host }) | ||
117 | await waitJobs(servers) | ||
118 | }) | ||
119 | |||
120 | it('Should not have notifications from this account', async function () { | ||
121 | await checkNotifications(servers[0], userToken1, []) | ||
122 | }) | ||
123 | |||
124 | it('Should have notifications of this account on user 2', async function () { | ||
125 | const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ] | ||
126 | |||
127 | await checkNotifications(servers[0], userToken2, notifs) | ||
128 | |||
129 | await servers[0].blocklist.removeFromMyBlocklist({ token: userToken1, account: 'user3@' + servers[1].host }) | ||
130 | }) | ||
131 | }) | ||
132 | |||
133 | describe('User blocks another server', function () { | ||
134 | |||
135 | before(async function () { | ||
136 | this.timeout(30000) | ||
137 | |||
138 | await resetState() | ||
139 | }) | ||
140 | |||
141 | it('Should have appropriate notifications', async function () { | ||
142 | const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ] | ||
143 | await checkNotifications(servers[0], userToken1, notifs) | ||
144 | }) | ||
145 | |||
146 | it('Should block an account', async function () { | ||
147 | await servers[0].blocklist.addToMyBlocklist({ token: userToken1, server: servers[1].host }) | ||
148 | await waitJobs(servers) | ||
149 | }) | ||
150 | |||
151 | it('Should not have notifications from this account', async function () { | ||
152 | await checkNotifications(servers[0], userToken1, []) | ||
153 | }) | ||
154 | |||
155 | it('Should have notifications of this account on user 2', async function () { | ||
156 | const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ] | ||
157 | |||
158 | await checkNotifications(servers[0], userToken2, notifs) | ||
159 | |||
160 | await servers[0].blocklist.removeFromMyBlocklist({ token: userToken1, server: servers[1].host }) | ||
161 | }) | ||
162 | }) | ||
163 | |||
164 | describe('Server blocks a user', function () { | ||
165 | |||
166 | before(async function () { | ||
167 | this.timeout(30000) | ||
168 | |||
169 | await resetState() | ||
170 | }) | ||
171 | |||
172 | it('Should have appropriate notifications', async function () { | ||
173 | { | ||
174 | const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ] | ||
175 | await checkNotifications(servers[0], userToken1, notifs) | ||
176 | } | ||
177 | |||
178 | { | ||
179 | const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ] | ||
180 | await checkNotifications(servers[0], userToken2, notifs) | ||
181 | } | ||
182 | }) | ||
183 | |||
184 | it('Should block an account', async function () { | ||
185 | await servers[0].blocklist.addToServerBlocklist({ account: 'user3@' + servers[1].host }) | ||
186 | await waitJobs(servers) | ||
187 | }) | ||
188 | |||
189 | it('Should not have notifications from this account', async function () { | ||
190 | await checkNotifications(servers[0], userToken1, []) | ||
191 | await checkNotifications(servers[0], userToken2, []) | ||
192 | |||
193 | await servers[0].blocklist.removeFromServerBlocklist({ account: 'user3@' + servers[1].host }) | ||
194 | }) | ||
195 | }) | ||
196 | |||
197 | describe('Server blocks a server', function () { | ||
198 | |||
199 | before(async function () { | ||
200 | this.timeout(30000) | ||
201 | |||
202 | await resetState() | ||
203 | }) | ||
204 | |||
205 | it('Should have appropriate notifications', async function () { | ||
206 | { | ||
207 | const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ] | ||
208 | await checkNotifications(servers[0], userToken1, notifs) | ||
209 | } | ||
210 | |||
211 | { | ||
212 | const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ] | ||
213 | await checkNotifications(servers[0], userToken2, notifs) | ||
214 | } | ||
215 | }) | ||
216 | |||
217 | it('Should block an account', async function () { | ||
218 | await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host }) | ||
219 | await waitJobs(servers) | ||
220 | }) | ||
221 | |||
222 | it('Should not have notifications from this account', async function () { | ||
223 | await checkNotifications(servers[0], userToken1, []) | ||
224 | await checkNotifications(servers[0], userToken2, []) | ||
225 | }) | ||
226 | }) | ||
227 | |||
228 | after(async function () { | ||
229 | await cleanupTests(servers) | ||
230 | }) | ||
231 | }) | ||
diff --git a/packages/tests/src/api/moderation/blocklist.ts b/packages/tests/src/api/moderation/blocklist.ts new file mode 100644 index 000000000..a84515241 --- /dev/null +++ b/packages/tests/src/api/moderation/blocklist.ts | |||
@@ -0,0 +1,902 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { UserNotificationType } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | BlocklistCommand, | ||
7 | cleanupTests, | ||
8 | CommentsCommand, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultAccountAvatar, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | async function checkAllVideos (server: PeerTubeServer, token: string) { | ||
18 | { | ||
19 | const { data } = await server.videos.listWithToken({ token }) | ||
20 | expect(data).to.have.lengthOf(5) | ||
21 | } | ||
22 | |||
23 | { | ||
24 | const { data } = await server.videos.list() | ||
25 | expect(data).to.have.lengthOf(5) | ||
26 | } | ||
27 | } | ||
28 | |||
29 | async function checkAllComments (server: PeerTubeServer, token: string, videoUUID: string) { | ||
30 | const { data } = await server.comments.listThreads({ videoId: videoUUID, start: 0, count: 25, sort: '-createdAt', token }) | ||
31 | |||
32 | const threads = data.filter(t => t.isDeleted === false) | ||
33 | expect(threads).to.have.lengthOf(2) | ||
34 | |||
35 | for (const thread of threads) { | ||
36 | const tree = await server.comments.getThread({ videoId: videoUUID, threadId: thread.id, token }) | ||
37 | expect(tree.children).to.have.lengthOf(1) | ||
38 | } | ||
39 | } | ||
40 | |||
41 | async function checkCommentNotification ( | ||
42 | mainServer: PeerTubeServer, | ||
43 | comment: { server: PeerTubeServer, token: string, videoUUID: string, text: string }, | ||
44 | check: 'presence' | 'absence' | ||
45 | ) { | ||
46 | const command = comment.server.comments | ||
47 | |||
48 | const { threadId, createdAt } = await command.createThread({ token: comment.token, videoId: comment.videoUUID, text: comment.text }) | ||
49 | |||
50 | await waitJobs([ mainServer, comment.server ]) | ||
51 | |||
52 | const { data } = await mainServer.notifications.list({ start: 0, count: 30 }) | ||
53 | const commentNotifications = data.filter(n => n.comment && n.comment.video.uuid === comment.videoUUID && n.createdAt >= createdAt) | ||
54 | |||
55 | if (check === 'presence') expect(commentNotifications).to.have.lengthOf(1) | ||
56 | else expect(commentNotifications).to.have.lengthOf(0) | ||
57 | |||
58 | await command.delete({ token: comment.token, videoId: comment.videoUUID, commentId: threadId }) | ||
59 | |||
60 | await waitJobs([ mainServer, comment.server ]) | ||
61 | } | ||
62 | |||
63 | describe('Test blocklist', function () { | ||
64 | let servers: PeerTubeServer[] | ||
65 | let videoUUID1: string | ||
66 | let videoUUID2: string | ||
67 | let videoUUID3: string | ||
68 | let userToken1: string | ||
69 | let userModeratorToken: string | ||
70 | let userToken2: string | ||
71 | |||
72 | let command: BlocklistCommand | ||
73 | let commentsCommand: CommentsCommand[] | ||
74 | |||
75 | before(async function () { | ||
76 | this.timeout(120000) | ||
77 | |||
78 | servers = await createMultipleServers(3) | ||
79 | await setAccessTokensToServers(servers) | ||
80 | await setDefaultAccountAvatar(servers) | ||
81 | |||
82 | command = servers[0].blocklist | ||
83 | commentsCommand = servers.map(s => s.comments) | ||
84 | |||
85 | { | ||
86 | const user = { username: 'user1', password: 'password' } | ||
87 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
88 | |||
89 | userToken1 = await servers[0].login.getAccessToken(user) | ||
90 | await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video user 1' } }) | ||
91 | } | ||
92 | |||
93 | { | ||
94 | const user = { username: 'moderator', password: 'password' } | ||
95 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
96 | |||
97 | userModeratorToken = await servers[0].login.getAccessToken(user) | ||
98 | } | ||
99 | |||
100 | { | ||
101 | const user = { username: 'user2', password: 'password' } | ||
102 | await servers[1].users.create({ username: user.username, password: user.password }) | ||
103 | |||
104 | userToken2 = await servers[1].login.getAccessToken(user) | ||
105 | await servers[1].videos.upload({ token: userToken2, attributes: { name: 'video user 2' } }) | ||
106 | } | ||
107 | |||
108 | { | ||
109 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video server 1' } }) | ||
110 | videoUUID1 = uuid | ||
111 | } | ||
112 | |||
113 | { | ||
114 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video server 2' } }) | ||
115 | videoUUID2 = uuid | ||
116 | } | ||
117 | |||
118 | { | ||
119 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 2 server 1' } }) | ||
120 | videoUUID3 = uuid | ||
121 | } | ||
122 | |||
123 | await doubleFollow(servers[0], servers[1]) | ||
124 | await doubleFollow(servers[0], servers[2]) | ||
125 | |||
126 | { | ||
127 | const created = await commentsCommand[0].createThread({ videoId: videoUUID1, text: 'comment root 1' }) | ||
128 | const reply = await commentsCommand[0].addReply({ | ||
129 | token: userToken1, | ||
130 | videoId: videoUUID1, | ||
131 | toCommentId: created.id, | ||
132 | text: 'comment user 1' | ||
133 | }) | ||
134 | await commentsCommand[0].addReply({ videoId: videoUUID1, toCommentId: reply.id, text: 'comment root 1' }) | ||
135 | } | ||
136 | |||
137 | { | ||
138 | const created = await commentsCommand[0].createThread({ token: userToken1, videoId: videoUUID1, text: 'comment user 1' }) | ||
139 | await commentsCommand[0].addReply({ videoId: videoUUID1, toCommentId: created.id, text: 'comment root 1' }) | ||
140 | } | ||
141 | |||
142 | await waitJobs(servers) | ||
143 | }) | ||
144 | |||
145 | describe('User blocklist', function () { | ||
146 | |||
147 | describe('When managing account blocklist', function () { | ||
148 | it('Should list all videos', function () { | ||
149 | return checkAllVideos(servers[0], servers[0].accessToken) | ||
150 | }) | ||
151 | |||
152 | it('Should list the comments', function () { | ||
153 | return checkAllComments(servers[0], servers[0].accessToken, videoUUID1) | ||
154 | }) | ||
155 | |||
156 | it('Should block a remote account', async function () { | ||
157 | await command.addToMyBlocklist({ account: 'user2@' + servers[1].host }) | ||
158 | }) | ||
159 | |||
160 | it('Should hide its videos', async function () { | ||
161 | const { data } = await servers[0].videos.listWithToken() | ||
162 | |||
163 | expect(data).to.have.lengthOf(4) | ||
164 | |||
165 | const v = data.find(v => v.name === 'video user 2') | ||
166 | expect(v).to.be.undefined | ||
167 | }) | ||
168 | |||
169 | it('Should block a local account', async function () { | ||
170 | await command.addToMyBlocklist({ account: 'user1' }) | ||
171 | }) | ||
172 | |||
173 | it('Should hide its videos', async function () { | ||
174 | const { data } = await servers[0].videos.listWithToken() | ||
175 | |||
176 | expect(data).to.have.lengthOf(3) | ||
177 | |||
178 | const v = data.find(v => v.name === 'video user 1') | ||
179 | expect(v).to.be.undefined | ||
180 | }) | ||
181 | |||
182 | it('Should hide its comments', async function () { | ||
183 | const { data } = await commentsCommand[0].listThreads({ | ||
184 | token: servers[0].accessToken, | ||
185 | videoId: videoUUID1, | ||
186 | start: 0, | ||
187 | count: 25, | ||
188 | sort: '-createdAt' | ||
189 | }) | ||
190 | |||
191 | expect(data).to.have.lengthOf(1) | ||
192 | expect(data[0].totalReplies).to.equal(1) | ||
193 | |||
194 | const t = data.find(t => t.text === 'comment user 1') | ||
195 | expect(t).to.be.undefined | ||
196 | |||
197 | for (const thread of data) { | ||
198 | const tree = await commentsCommand[0].getThread({ | ||
199 | videoId: videoUUID1, | ||
200 | threadId: thread.id, | ||
201 | token: servers[0].accessToken | ||
202 | }) | ||
203 | expect(tree.children).to.have.lengthOf(0) | ||
204 | } | ||
205 | }) | ||
206 | |||
207 | it('Should not have notifications from blocked accounts', async function () { | ||
208 | this.timeout(20000) | ||
209 | |||
210 | { | ||
211 | const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' } | ||
212 | await checkCommentNotification(servers[0], comment, 'absence') | ||
213 | } | ||
214 | |||
215 | { | ||
216 | const comment = { | ||
217 | server: servers[0], | ||
218 | token: userToken1, | ||
219 | videoUUID: videoUUID2, | ||
220 | text: 'hello @root@' + servers[0].host | ||
221 | } | ||
222 | await checkCommentNotification(servers[0], comment, 'absence') | ||
223 | } | ||
224 | }) | ||
225 | |||
226 | it('Should list all the videos with another user', async function () { | ||
227 | return checkAllVideos(servers[0], userToken1) | ||
228 | }) | ||
229 | |||
230 | it('Should list blocked accounts', async function () { | ||
231 | { | ||
232 | const body = await command.listMyAccountBlocklist({ start: 0, count: 1, sort: 'createdAt' }) | ||
233 | expect(body.total).to.equal(2) | ||
234 | |||
235 | const block = body.data[0] | ||
236 | expect(block.byAccount.displayName).to.equal('root') | ||
237 | expect(block.byAccount.name).to.equal('root') | ||
238 | expect(block.blockedAccount.displayName).to.equal('user2') | ||
239 | expect(block.blockedAccount.name).to.equal('user2') | ||
240 | expect(block.blockedAccount.host).to.equal('' + servers[1].host) | ||
241 | } | ||
242 | |||
243 | { | ||
244 | const body = await command.listMyAccountBlocklist({ start: 1, count: 2, sort: 'createdAt' }) | ||
245 | expect(body.total).to.equal(2) | ||
246 | |||
247 | const block = body.data[0] | ||
248 | expect(block.byAccount.displayName).to.equal('root') | ||
249 | expect(block.byAccount.name).to.equal('root') | ||
250 | expect(block.blockedAccount.displayName).to.equal('user1') | ||
251 | expect(block.blockedAccount.name).to.equal('user1') | ||
252 | expect(block.blockedAccount.host).to.equal('' + servers[0].host) | ||
253 | } | ||
254 | }) | ||
255 | |||
256 | it('Should search blocked accounts', async function () { | ||
257 | const body = await command.listMyAccountBlocklist({ start: 0, count: 10, search: 'user2' }) | ||
258 | expect(body.total).to.equal(1) | ||
259 | |||
260 | expect(body.data[0].blockedAccount.name).to.equal('user2') | ||
261 | }) | ||
262 | |||
263 | it('Should get blocked status', async function () { | ||
264 | const remoteHandle = 'user2@' + servers[1].host | ||
265 | const localHandle = 'user1@' + servers[0].host | ||
266 | const unknownHandle = 'user5@' + servers[0].host | ||
267 | |||
268 | { | ||
269 | const status = await command.getStatus({ accounts: [ remoteHandle ] }) | ||
270 | expect(Object.keys(status.accounts)).to.have.lengthOf(1) | ||
271 | expect(status.accounts[remoteHandle].blockedByUser).to.be.false | ||
272 | expect(status.accounts[remoteHandle].blockedByServer).to.be.false | ||
273 | |||
274 | expect(Object.keys(status.hosts)).to.have.lengthOf(0) | ||
275 | } | ||
276 | |||
277 | { | ||
278 | const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ remoteHandle ] }) | ||
279 | expect(Object.keys(status.accounts)).to.have.lengthOf(1) | ||
280 | expect(status.accounts[remoteHandle].blockedByUser).to.be.true | ||
281 | expect(status.accounts[remoteHandle].blockedByServer).to.be.false | ||
282 | |||
283 | expect(Object.keys(status.hosts)).to.have.lengthOf(0) | ||
284 | } | ||
285 | |||
286 | { | ||
287 | const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ localHandle, remoteHandle, unknownHandle ] }) | ||
288 | expect(Object.keys(status.accounts)).to.have.lengthOf(3) | ||
289 | |||
290 | for (const handle of [ localHandle, remoteHandle ]) { | ||
291 | expect(status.accounts[handle].blockedByUser).to.be.true | ||
292 | expect(status.accounts[handle].blockedByServer).to.be.false | ||
293 | } | ||
294 | |||
295 | expect(status.accounts[unknownHandle].blockedByUser).to.be.false | ||
296 | expect(status.accounts[unknownHandle].blockedByServer).to.be.false | ||
297 | |||
298 | expect(Object.keys(status.hosts)).to.have.lengthOf(0) | ||
299 | } | ||
300 | }) | ||
301 | |||
302 | it('Should not allow a remote blocked user to comment my videos', async function () { | ||
303 | this.timeout(60000) | ||
304 | |||
305 | { | ||
306 | await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID3, text: 'comment user 2' }) | ||
307 | await waitJobs(servers) | ||
308 | |||
309 | await commentsCommand[0].createThread({ token: servers[0].accessToken, videoId: videoUUID3, text: 'uploader' }) | ||
310 | await waitJobs(servers) | ||
311 | |||
312 | const commentId = await commentsCommand[1].findCommentId({ videoId: videoUUID3, text: 'uploader' }) | ||
313 | const message = 'reply by user 2' | ||
314 | const reply = await commentsCommand[1].addReply({ token: userToken2, videoId: videoUUID3, toCommentId: commentId, text: message }) | ||
315 | await commentsCommand[1].addReply({ videoId: videoUUID3, toCommentId: reply.id, text: 'another reply' }) | ||
316 | |||
317 | await waitJobs(servers) | ||
318 | } | ||
319 | |||
320 | // Server 2 has all the comments | ||
321 | { | ||
322 | const { data } = await commentsCommand[1].listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' }) | ||
323 | |||
324 | expect(data).to.have.lengthOf(2) | ||
325 | expect(data[0].text).to.equal('uploader') | ||
326 | expect(data[1].text).to.equal('comment user 2') | ||
327 | |||
328 | const tree = await commentsCommand[1].getThread({ videoId: videoUUID3, threadId: data[0].id }) | ||
329 | expect(tree.children).to.have.lengthOf(1) | ||
330 | expect(tree.children[0].comment.text).to.equal('reply by user 2') | ||
331 | expect(tree.children[0].children).to.have.lengthOf(1) | ||
332 | expect(tree.children[0].children[0].comment.text).to.equal('another reply') | ||
333 | } | ||
334 | |||
335 | // Server 1 and 3 should only have uploader comments | ||
336 | for (const server of [ servers[0], servers[2] ]) { | ||
337 | const { data } = await server.comments.listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' }) | ||
338 | |||
339 | expect(data).to.have.lengthOf(1) | ||
340 | expect(data[0].text).to.equal('uploader') | ||
341 | |||
342 | const tree = await server.comments.getThread({ videoId: videoUUID3, threadId: data[0].id }) | ||
343 | |||
344 | if (server.serverNumber === 1) expect(tree.children).to.have.lengthOf(0) | ||
345 | else expect(tree.children).to.have.lengthOf(1) | ||
346 | } | ||
347 | }) | ||
348 | |||
349 | it('Should unblock the remote account', async function () { | ||
350 | await command.removeFromMyBlocklist({ account: 'user2@' + servers[1].host }) | ||
351 | }) | ||
352 | |||
353 | it('Should display its videos', async function () { | ||
354 | const { data } = await servers[0].videos.listWithToken() | ||
355 | expect(data).to.have.lengthOf(4) | ||
356 | |||
357 | const v = data.find(v => v.name === 'video user 2') | ||
358 | expect(v).not.to.be.undefined | ||
359 | }) | ||
360 | |||
361 | it('Should display its comments on my video', async function () { | ||
362 | for (const server of servers) { | ||
363 | const { data } = await server.comments.listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' }) | ||
364 | |||
365 | // Server 3 should not have 2 comment threads, because server 1 did not forward the server 2 comment | ||
366 | if (server.serverNumber === 3) { | ||
367 | expect(data).to.have.lengthOf(1) | ||
368 | continue | ||
369 | } | ||
370 | |||
371 | expect(data).to.have.lengthOf(2) | ||
372 | expect(data[0].text).to.equal('uploader') | ||
373 | expect(data[1].text).to.equal('comment user 2') | ||
374 | |||
375 | const tree = await server.comments.getThread({ videoId: videoUUID3, threadId: data[0].id }) | ||
376 | expect(tree.children).to.have.lengthOf(1) | ||
377 | expect(tree.children[0].comment.text).to.equal('reply by user 2') | ||
378 | expect(tree.children[0].children).to.have.lengthOf(1) | ||
379 | expect(tree.children[0].children[0].comment.text).to.equal('another reply') | ||
380 | } | ||
381 | }) | ||
382 | |||
383 | it('Should unblock the local account', async function () { | ||
384 | await command.removeFromMyBlocklist({ account: 'user1' }) | ||
385 | }) | ||
386 | |||
387 | it('Should display its comments', function () { | ||
388 | return checkAllComments(servers[0], servers[0].accessToken, videoUUID1) | ||
389 | }) | ||
390 | |||
391 | it('Should have a notification from a non blocked account', async function () { | ||
392 | this.timeout(20000) | ||
393 | |||
394 | { | ||
395 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } | ||
396 | await checkCommentNotification(servers[0], comment, 'presence') | ||
397 | } | ||
398 | |||
399 | { | ||
400 | const comment = { | ||
401 | server: servers[0], | ||
402 | token: userToken1, | ||
403 | videoUUID: videoUUID2, | ||
404 | text: 'hello @root@' + servers[0].host | ||
405 | } | ||
406 | await checkCommentNotification(servers[0], comment, 'presence') | ||
407 | } | ||
408 | }) | ||
409 | }) | ||
410 | |||
411 | describe('When managing server blocklist', function () { | ||
412 | |||
413 | it('Should list all videos', function () { | ||
414 | return checkAllVideos(servers[0], servers[0].accessToken) | ||
415 | }) | ||
416 | |||
417 | it('Should list the comments', function () { | ||
418 | return checkAllComments(servers[0], servers[0].accessToken, videoUUID1) | ||
419 | }) | ||
420 | |||
421 | it('Should block a remote server', async function () { | ||
422 | await command.addToMyBlocklist({ server: '' + servers[1].host }) | ||
423 | }) | ||
424 | |||
425 | it('Should hide its videos', async function () { | ||
426 | const { data } = await servers[0].videos.listWithToken() | ||
427 | |||
428 | expect(data).to.have.lengthOf(3) | ||
429 | |||
430 | const v1 = data.find(v => v.name === 'video user 2') | ||
431 | const v2 = data.find(v => v.name === 'video server 2') | ||
432 | |||
433 | expect(v1).to.be.undefined | ||
434 | expect(v2).to.be.undefined | ||
435 | }) | ||
436 | |||
437 | it('Should list all the videos with another user', async function () { | ||
438 | return checkAllVideos(servers[0], userToken1) | ||
439 | }) | ||
440 | |||
441 | it('Should hide its comments', async function () { | ||
442 | const { id } = await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID1, text: 'hidden comment 2' }) | ||
443 | |||
444 | await waitJobs(servers) | ||
445 | |||
446 | await checkAllComments(servers[0], servers[0].accessToken, videoUUID1) | ||
447 | |||
448 | await commentsCommand[1].delete({ token: userToken2, videoId: videoUUID1, commentId: id }) | ||
449 | }) | ||
450 | |||
451 | it('Should not have notifications from blocked server', async function () { | ||
452 | this.timeout(20000) | ||
453 | |||
454 | { | ||
455 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' } | ||
456 | await checkCommentNotification(servers[0], comment, 'absence') | ||
457 | } | ||
458 | |||
459 | { | ||
460 | const comment = { | ||
461 | server: servers[1], | ||
462 | token: userToken2, | ||
463 | videoUUID: videoUUID1, | ||
464 | text: 'hello @root@' + servers[0].host | ||
465 | } | ||
466 | await checkCommentNotification(servers[0], comment, 'absence') | ||
467 | } | ||
468 | }) | ||
469 | |||
470 | it('Should list blocked servers', async function () { | ||
471 | const body = await command.listMyServerBlocklist({ start: 0, count: 1, sort: 'createdAt' }) | ||
472 | expect(body.total).to.equal(1) | ||
473 | |||
474 | const block = body.data[0] | ||
475 | expect(block.byAccount.displayName).to.equal('root') | ||
476 | expect(block.byAccount.name).to.equal('root') | ||
477 | expect(block.blockedServer.host).to.equal('' + servers[1].host) | ||
478 | }) | ||
479 | |||
480 | it('Should search blocked servers', async function () { | ||
481 | const body = await command.listMyServerBlocklist({ start: 0, count: 10, search: servers[1].host }) | ||
482 | expect(body.total).to.equal(1) | ||
483 | |||
484 | expect(body.data[0].blockedServer.host).to.equal(servers[1].host) | ||
485 | }) | ||
486 | |||
487 | it('Should get blocklist status', async function () { | ||
488 | const blockedServer = servers[1].host | ||
489 | const notBlockedServer = 'example.com' | ||
490 | |||
491 | { | ||
492 | const status = await command.getStatus({ hosts: [ blockedServer, notBlockedServer ] }) | ||
493 | expect(Object.keys(status.accounts)).to.have.lengthOf(0) | ||
494 | |||
495 | expect(Object.keys(status.hosts)).to.have.lengthOf(2) | ||
496 | expect(status.hosts[blockedServer].blockedByUser).to.be.false | ||
497 | expect(status.hosts[blockedServer].blockedByServer).to.be.false | ||
498 | |||
499 | expect(status.hosts[notBlockedServer].blockedByUser).to.be.false | ||
500 | expect(status.hosts[notBlockedServer].blockedByServer).to.be.false | ||
501 | } | ||
502 | |||
503 | { | ||
504 | const status = await command.getStatus({ token: servers[0].accessToken, hosts: [ blockedServer, notBlockedServer ] }) | ||
505 | expect(Object.keys(status.accounts)).to.have.lengthOf(0) | ||
506 | |||
507 | expect(Object.keys(status.hosts)).to.have.lengthOf(2) | ||
508 | expect(status.hosts[blockedServer].blockedByUser).to.be.true | ||
509 | expect(status.hosts[blockedServer].blockedByServer).to.be.false | ||
510 | |||
511 | expect(status.hosts[notBlockedServer].blockedByUser).to.be.false | ||
512 | expect(status.hosts[notBlockedServer].blockedByServer).to.be.false | ||
513 | } | ||
514 | }) | ||
515 | |||
516 | it('Should unblock the remote server', async function () { | ||
517 | await command.removeFromMyBlocklist({ server: '' + servers[1].host }) | ||
518 | }) | ||
519 | |||
520 | it('Should display its videos', function () { | ||
521 | return checkAllVideos(servers[0], servers[0].accessToken) | ||
522 | }) | ||
523 | |||
524 | it('Should display its comments', function () { | ||
525 | return checkAllComments(servers[0], servers[0].accessToken, videoUUID1) | ||
526 | }) | ||
527 | |||
528 | it('Should have notification from unblocked server', async function () { | ||
529 | this.timeout(20000) | ||
530 | |||
531 | { | ||
532 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } | ||
533 | await checkCommentNotification(servers[0], comment, 'presence') | ||
534 | } | ||
535 | |||
536 | { | ||
537 | const comment = { | ||
538 | server: servers[1], | ||
539 | token: userToken2, | ||
540 | videoUUID: videoUUID1, | ||
541 | text: 'hello @root@' + servers[0].host | ||
542 | } | ||
543 | await checkCommentNotification(servers[0], comment, 'presence') | ||
544 | } | ||
545 | }) | ||
546 | }) | ||
547 | }) | ||
548 | |||
549 | describe('Server blocklist', function () { | ||
550 | |||
551 | describe('When managing account blocklist', function () { | ||
552 | it('Should list all videos', async function () { | ||
553 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
554 | await checkAllVideos(servers[0], token) | ||
555 | } | ||
556 | }) | ||
557 | |||
558 | it('Should list the comments', async function () { | ||
559 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
560 | await checkAllComments(servers[0], token, videoUUID1) | ||
561 | } | ||
562 | }) | ||
563 | |||
564 | it('Should block a remote account', async function () { | ||
565 | await command.addToServerBlocklist({ account: 'user2@' + servers[1].host }) | ||
566 | }) | ||
567 | |||
568 | it('Should hide its videos', async function () { | ||
569 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
570 | const { data } = await servers[0].videos.listWithToken({ token }) | ||
571 | |||
572 | expect(data).to.have.lengthOf(4) | ||
573 | |||
574 | const v = data.find(v => v.name === 'video user 2') | ||
575 | expect(v).to.be.undefined | ||
576 | } | ||
577 | }) | ||
578 | |||
579 | it('Should block a local account', async function () { | ||
580 | await command.addToServerBlocklist({ account: 'user1' }) | ||
581 | }) | ||
582 | |||
583 | it('Should hide its videos', async function () { | ||
584 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
585 | const { data } = await servers[0].videos.listWithToken({ token }) | ||
586 | |||
587 | expect(data).to.have.lengthOf(3) | ||
588 | |||
589 | const v = data.find(v => v.name === 'video user 1') | ||
590 | expect(v).to.be.undefined | ||
591 | } | ||
592 | }) | ||
593 | |||
594 | it('Should hide its comments', async function () { | ||
595 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
596 | const { data } = await commentsCommand[0].listThreads({ videoId: videoUUID1, count: 20, sort: '-createdAt', token }) | ||
597 | const threads = data.filter(t => t.isDeleted === false) | ||
598 | |||
599 | expect(threads).to.have.lengthOf(1) | ||
600 | expect(threads[0].totalReplies).to.equal(1) | ||
601 | |||
602 | const t = threads.find(t => t.text === 'comment user 1') | ||
603 | expect(t).to.be.undefined | ||
604 | |||
605 | for (const thread of threads) { | ||
606 | const tree = await commentsCommand[0].getThread({ videoId: videoUUID1, threadId: thread.id, token }) | ||
607 | expect(tree.children).to.have.lengthOf(0) | ||
608 | } | ||
609 | } | ||
610 | }) | ||
611 | |||
612 | it('Should not have notification from blocked accounts by instance', async function () { | ||
613 | this.timeout(20000) | ||
614 | |||
615 | { | ||
616 | const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' } | ||
617 | await checkCommentNotification(servers[0], comment, 'absence') | ||
618 | } | ||
619 | |||
620 | { | ||
621 | const comment = { | ||
622 | server: servers[1], | ||
623 | token: userToken2, | ||
624 | videoUUID: videoUUID1, | ||
625 | text: 'hello @root@' + servers[0].host | ||
626 | } | ||
627 | await checkCommentNotification(servers[0], comment, 'absence') | ||
628 | } | ||
629 | }) | ||
630 | |||
631 | it('Should list blocked accounts', async function () { | ||
632 | { | ||
633 | const body = await command.listServerAccountBlocklist({ start: 0, count: 1, sort: 'createdAt' }) | ||
634 | expect(body.total).to.equal(2) | ||
635 | |||
636 | const block = body.data[0] | ||
637 | expect(block.byAccount.displayName).to.equal('peertube') | ||
638 | expect(block.byAccount.name).to.equal('peertube') | ||
639 | expect(block.blockedAccount.displayName).to.equal('user2') | ||
640 | expect(block.blockedAccount.name).to.equal('user2') | ||
641 | expect(block.blockedAccount.host).to.equal('' + servers[1].host) | ||
642 | } | ||
643 | |||
644 | { | ||
645 | const body = await command.listServerAccountBlocklist({ start: 1, count: 2, sort: 'createdAt' }) | ||
646 | expect(body.total).to.equal(2) | ||
647 | |||
648 | const block = body.data[0] | ||
649 | expect(block.byAccount.displayName).to.equal('peertube') | ||
650 | expect(block.byAccount.name).to.equal('peertube') | ||
651 | expect(block.blockedAccount.displayName).to.equal('user1') | ||
652 | expect(block.blockedAccount.name).to.equal('user1') | ||
653 | expect(block.blockedAccount.host).to.equal('' + servers[0].host) | ||
654 | } | ||
655 | }) | ||
656 | |||
657 | it('Should search blocked accounts', async function () { | ||
658 | const body = await command.listServerAccountBlocklist({ start: 0, count: 10, search: 'user2' }) | ||
659 | expect(body.total).to.equal(1) | ||
660 | |||
661 | expect(body.data[0].blockedAccount.name).to.equal('user2') | ||
662 | }) | ||
663 | |||
664 | it('Should get blocked status', async function () { | ||
665 | const remoteHandle = 'user2@' + servers[1].host | ||
666 | const localHandle = 'user1@' + servers[0].host | ||
667 | const unknownHandle = 'user5@' + servers[0].host | ||
668 | |||
669 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
670 | const status = await command.getStatus({ token, accounts: [ localHandle, remoteHandle, unknownHandle ] }) | ||
671 | expect(Object.keys(status.accounts)).to.have.lengthOf(3) | ||
672 | |||
673 | for (const handle of [ localHandle, remoteHandle ]) { | ||
674 | expect(status.accounts[handle].blockedByUser).to.be.false | ||
675 | expect(status.accounts[handle].blockedByServer).to.be.true | ||
676 | } | ||
677 | |||
678 | expect(status.accounts[unknownHandle].blockedByUser).to.be.false | ||
679 | expect(status.accounts[unknownHandle].blockedByServer).to.be.false | ||
680 | |||
681 | expect(Object.keys(status.hosts)).to.have.lengthOf(0) | ||
682 | } | ||
683 | }) | ||
684 | |||
685 | it('Should unblock the remote account', async function () { | ||
686 | await command.removeFromServerBlocklist({ account: 'user2@' + servers[1].host }) | ||
687 | }) | ||
688 | |||
689 | it('Should display its videos', async function () { | ||
690 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
691 | const { data } = await servers[0].videos.listWithToken({ token }) | ||
692 | expect(data).to.have.lengthOf(4) | ||
693 | |||
694 | const v = data.find(v => v.name === 'video user 2') | ||
695 | expect(v).not.to.be.undefined | ||
696 | } | ||
697 | }) | ||
698 | |||
699 | it('Should unblock the local account', async function () { | ||
700 | await command.removeFromServerBlocklist({ account: 'user1' }) | ||
701 | }) | ||
702 | |||
703 | it('Should display its comments', async function () { | ||
704 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
705 | await checkAllComments(servers[0], token, videoUUID1) | ||
706 | } | ||
707 | }) | ||
708 | |||
709 | it('Should have notifications from unblocked accounts', async function () { | ||
710 | this.timeout(20000) | ||
711 | |||
712 | { | ||
713 | const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'displayed comment' } | ||
714 | await checkCommentNotification(servers[0], comment, 'presence') | ||
715 | } | ||
716 | |||
717 | { | ||
718 | const comment = { | ||
719 | server: servers[1], | ||
720 | token: userToken2, | ||
721 | videoUUID: videoUUID1, | ||
722 | text: 'hello @root@' + servers[0].host | ||
723 | } | ||
724 | await checkCommentNotification(servers[0], comment, 'presence') | ||
725 | } | ||
726 | }) | ||
727 | }) | ||
728 | |||
729 | describe('When managing server blocklist', function () { | ||
730 | |||
731 | it('Should list all videos', async function () { | ||
732 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
733 | await checkAllVideos(servers[0], token) | ||
734 | } | ||
735 | }) | ||
736 | |||
737 | it('Should list the comments', async function () { | ||
738 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
739 | await checkAllComments(servers[0], token, videoUUID1) | ||
740 | } | ||
741 | }) | ||
742 | |||
743 | it('Should block a remote server', async function () { | ||
744 | await command.addToServerBlocklist({ server: '' + servers[1].host }) | ||
745 | }) | ||
746 | |||
747 | it('Should hide its videos', async function () { | ||
748 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
749 | const requests = [ | ||
750 | servers[0].videos.list(), | ||
751 | servers[0].videos.listWithToken({ token }) | ||
752 | ] | ||
753 | |||
754 | for (const req of requests) { | ||
755 | const { data } = await req | ||
756 | expect(data).to.have.lengthOf(3) | ||
757 | |||
758 | const v1 = data.find(v => v.name === 'video user 2') | ||
759 | const v2 = data.find(v => v.name === 'video server 2') | ||
760 | |||
761 | expect(v1).to.be.undefined | ||
762 | expect(v2).to.be.undefined | ||
763 | } | ||
764 | } | ||
765 | }) | ||
766 | |||
767 | it('Should hide its comments', async function () { | ||
768 | const { id } = await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID1, text: 'hidden comment 2' }) | ||
769 | |||
770 | await waitJobs(servers) | ||
771 | |||
772 | await checkAllComments(servers[0], servers[0].accessToken, videoUUID1) | ||
773 | |||
774 | await commentsCommand[1].delete({ token: userToken2, videoId: videoUUID1, commentId: id }) | ||
775 | }) | ||
776 | |||
777 | it('Should not have notification from blocked instances by instance', async function () { | ||
778 | this.timeout(50000) | ||
779 | |||
780 | { | ||
781 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' } | ||
782 | await checkCommentNotification(servers[0], comment, 'absence') | ||
783 | } | ||
784 | |||
785 | { | ||
786 | const comment = { | ||
787 | server: servers[1], | ||
788 | token: userToken2, | ||
789 | videoUUID: videoUUID1, | ||
790 | text: 'hello @root@' + servers[0].host | ||
791 | } | ||
792 | await checkCommentNotification(servers[0], comment, 'absence') | ||
793 | } | ||
794 | |||
795 | { | ||
796 | const now = new Date() | ||
797 | await servers[1].follows.unfollow({ target: servers[0] }) | ||
798 | await waitJobs(servers) | ||
799 | await servers[1].follows.follow({ hosts: [ servers[0].host ] }) | ||
800 | |||
801 | await waitJobs(servers) | ||
802 | |||
803 | const { data } = await servers[0].notifications.list({ start: 0, count: 30 }) | ||
804 | const commentNotifications = data.filter(n => { | ||
805 | return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && n.createdAt >= now.toISOString() | ||
806 | }) | ||
807 | |||
808 | expect(commentNotifications).to.have.lengthOf(0) | ||
809 | } | ||
810 | }) | ||
811 | |||
812 | it('Should list blocked servers', async function () { | ||
813 | const body = await command.listServerServerBlocklist({ start: 0, count: 1, sort: 'createdAt' }) | ||
814 | expect(body.total).to.equal(1) | ||
815 | |||
816 | const block = body.data[0] | ||
817 | expect(block.byAccount.displayName).to.equal('peertube') | ||
818 | expect(block.byAccount.name).to.equal('peertube') | ||
819 | expect(block.blockedServer.host).to.equal('' + servers[1].host) | ||
820 | }) | ||
821 | |||
822 | it('Should search blocked servers', async function () { | ||
823 | const body = await command.listServerServerBlocklist({ start: 0, count: 10, search: servers[1].host }) | ||
824 | expect(body.total).to.equal(1) | ||
825 | |||
826 | expect(body.data[0].blockedServer.host).to.equal(servers[1].host) | ||
827 | }) | ||
828 | |||
829 | it('Should get blocklist status', async function () { | ||
830 | const blockedServer = servers[1].host | ||
831 | const notBlockedServer = 'example.com' | ||
832 | |||
833 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
834 | const status = await command.getStatus({ token, hosts: [ blockedServer, notBlockedServer ] }) | ||
835 | expect(Object.keys(status.accounts)).to.have.lengthOf(0) | ||
836 | |||
837 | expect(Object.keys(status.hosts)).to.have.lengthOf(2) | ||
838 | expect(status.hosts[blockedServer].blockedByUser).to.be.false | ||
839 | expect(status.hosts[blockedServer].blockedByServer).to.be.true | ||
840 | |||
841 | expect(status.hosts[notBlockedServer].blockedByUser).to.be.false | ||
842 | expect(status.hosts[notBlockedServer].blockedByServer).to.be.false | ||
843 | } | ||
844 | }) | ||
845 | |||
846 | it('Should unblock the remote server', async function () { | ||
847 | await command.removeFromServerBlocklist({ server: '' + servers[1].host }) | ||
848 | }) | ||
849 | |||
850 | it('Should list all videos', async function () { | ||
851 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
852 | await checkAllVideos(servers[0], token) | ||
853 | } | ||
854 | }) | ||
855 | |||
856 | it('Should list the comments', async function () { | ||
857 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | ||
858 | await checkAllComments(servers[0], token, videoUUID1) | ||
859 | } | ||
860 | }) | ||
861 | |||
862 | it('Should have notification from unblocked instances', async function () { | ||
863 | this.timeout(50000) | ||
864 | |||
865 | { | ||
866 | const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } | ||
867 | await checkCommentNotification(servers[0], comment, 'presence') | ||
868 | } | ||
869 | |||
870 | { | ||
871 | const comment = { | ||
872 | server: servers[1], | ||
873 | token: userToken2, | ||
874 | videoUUID: videoUUID1, | ||
875 | text: 'hello @root@' + servers[0].host | ||
876 | } | ||
877 | await checkCommentNotification(servers[0], comment, 'presence') | ||
878 | } | ||
879 | |||
880 | { | ||
881 | const now = new Date() | ||
882 | await servers[1].follows.unfollow({ target: servers[0] }) | ||
883 | await waitJobs(servers) | ||
884 | await servers[1].follows.follow({ hosts: [ servers[0].host ] }) | ||
885 | |||
886 | await waitJobs(servers) | ||
887 | |||
888 | const { data } = await servers[0].notifications.list({ start: 0, count: 30 }) | ||
889 | const commentNotifications = data.filter(n => { | ||
890 | return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && n.createdAt >= now.toISOString() | ||
891 | }) | ||
892 | |||
893 | expect(commentNotifications).to.have.lengthOf(1) | ||
894 | } | ||
895 | }) | ||
896 | }) | ||
897 | }) | ||
898 | |||
899 | after(async function () { | ||
900 | await cleanupTests(servers) | ||
901 | }) | ||
902 | }) | ||
diff --git a/packages/tests/src/api/moderation/index.ts b/packages/tests/src/api/moderation/index.ts new file mode 100644 index 000000000..e3794d01e --- /dev/null +++ b/packages/tests/src/api/moderation/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './abuses.js' | ||
2 | export * from './blocklist-notification.js' | ||
3 | export * from './blocklist.js' | ||
4 | export * from './video-blacklist.js' | ||
diff --git a/packages/tests/src/api/moderation/video-blacklist.ts b/packages/tests/src/api/moderation/video-blacklist.ts new file mode 100644 index 000000000..341dadad0 --- /dev/null +++ b/packages/tests/src/api/moderation/video-blacklist.ts | |||
@@ -0,0 +1,414 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
5 | import { sortObjectComparator } from '@peertube/peertube-core-utils' | ||
6 | import { UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | BlacklistCommand, | ||
9 | cleanupTests, | ||
10 | createMultipleServers, | ||
11 | doubleFollow, | ||
12 | killallServers, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultChannelAvatar, | ||
16 | waitJobs | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | |||
19 | describe('Test video blacklist', function () { | ||
20 | let servers: PeerTubeServer[] = [] | ||
21 | let videoId: number | ||
22 | let command: BlacklistCommand | ||
23 | |||
24 | async function blacklistVideosOnServer (server: PeerTubeServer) { | ||
25 | const { data } = await server.videos.list() | ||
26 | |||
27 | for (const video of data) { | ||
28 | await server.blacklist.add({ videoId: video.id, reason: 'super reason' }) | ||
29 | } | ||
30 | } | ||
31 | |||
32 | before(async function () { | ||
33 | this.timeout(120000) | ||
34 | |||
35 | // Run servers | ||
36 | servers = await createMultipleServers(2) | ||
37 | |||
38 | // Get the access tokens | ||
39 | await setAccessTokensToServers(servers) | ||
40 | |||
41 | // Server 1 and server 2 follow each other | ||
42 | await doubleFollow(servers[0], servers[1]) | ||
43 | await setDefaultChannelAvatar(servers[0]) | ||
44 | |||
45 | // Upload 2 videos on server 2 | ||
46 | await servers[1].videos.upload({ attributes: { name: 'My 1st video', description: 'A video on server 2' } }) | ||
47 | await servers[1].videos.upload({ attributes: { name: 'My 2nd video', description: 'A video on server 2' } }) | ||
48 | |||
49 | // Wait videos propagation, server 2 has transcoding enabled | ||
50 | await waitJobs(servers) | ||
51 | |||
52 | command = servers[0].blacklist | ||
53 | |||
54 | // Blacklist the two videos on server 1 | ||
55 | await blacklistVideosOnServer(servers[0]) | ||
56 | }) | ||
57 | |||
58 | describe('When listing/searching videos', function () { | ||
59 | |||
60 | it('Should not have the video blacklisted in videos list/search on server 1', async function () { | ||
61 | { | ||
62 | const { total, data } = await servers[0].videos.list() | ||
63 | |||
64 | expect(total).to.equal(0) | ||
65 | expect(data).to.be.an('array') | ||
66 | expect(data.length).to.equal(0) | ||
67 | } | ||
68 | |||
69 | { | ||
70 | const body = await servers[0].search.searchVideos({ search: 'video' }) | ||
71 | |||
72 | expect(body.total).to.equal(0) | ||
73 | expect(body.data).to.be.an('array') | ||
74 | expect(body.data.length).to.equal(0) | ||
75 | } | ||
76 | }) | ||
77 | |||
78 | it('Should have the blacklisted video in videos list/search on server 2', async function () { | ||
79 | { | ||
80 | const { total, data } = await servers[1].videos.list() | ||
81 | |||
82 | expect(total).to.equal(2) | ||
83 | expect(data).to.be.an('array') | ||
84 | expect(data.length).to.equal(2) | ||
85 | } | ||
86 | |||
87 | { | ||
88 | const body = await servers[1].search.searchVideos({ search: 'video' }) | ||
89 | |||
90 | expect(body.total).to.equal(2) | ||
91 | expect(body.data).to.be.an('array') | ||
92 | expect(body.data.length).to.equal(2) | ||
93 | } | ||
94 | }) | ||
95 | }) | ||
96 | |||
97 | describe('When listing manually blacklisted videos', function () { | ||
98 | it('Should display all the blacklisted videos', async function () { | ||
99 | const body = await command.list() | ||
100 | expect(body.total).to.equal(2) | ||
101 | |||
102 | const blacklistedVideos = body.data | ||
103 | expect(blacklistedVideos).to.be.an('array') | ||
104 | expect(blacklistedVideos.length).to.equal(2) | ||
105 | |||
106 | for (const blacklistedVideo of blacklistedVideos) { | ||
107 | expect(blacklistedVideo.reason).to.equal('super reason') | ||
108 | videoId = blacklistedVideo.video.id | ||
109 | } | ||
110 | }) | ||
111 | |||
112 | it('Should display all the blacklisted videos when applying manual type filter', async function () { | ||
113 | const body = await command.list({ type: VideoBlacklistType.MANUAL }) | ||
114 | expect(body.total).to.equal(2) | ||
115 | |||
116 | const blacklistedVideos = body.data | ||
117 | expect(blacklistedVideos).to.be.an('array') | ||
118 | expect(blacklistedVideos.length).to.equal(2) | ||
119 | }) | ||
120 | |||
121 | it('Should display nothing when applying automatic type filter', async function () { | ||
122 | const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED }) | ||
123 | expect(body.total).to.equal(0) | ||
124 | |||
125 | const blacklistedVideos = body.data | ||
126 | expect(blacklistedVideos).to.be.an('array') | ||
127 | expect(blacklistedVideos.length).to.equal(0) | ||
128 | }) | ||
129 | |||
130 | it('Should get the correct sort when sorting by descending id', async function () { | ||
131 | const body = await command.list({ sort: '-id' }) | ||
132 | expect(body.total).to.equal(2) | ||
133 | |||
134 | const blacklistedVideos = body.data | ||
135 | expect(blacklistedVideos).to.be.an('array') | ||
136 | expect(blacklistedVideos.length).to.equal(2) | ||
137 | |||
138 | const result = [ ...body.data ].sort(sortObjectComparator('id', 'desc')) | ||
139 | expect(blacklistedVideos).to.deep.equal(result) | ||
140 | }) | ||
141 | |||
142 | it('Should get the correct sort when sorting by descending video name', async function () { | ||
143 | const body = await command.list({ sort: '-name' }) | ||
144 | expect(body.total).to.equal(2) | ||
145 | |||
146 | const blacklistedVideos = body.data | ||
147 | expect(blacklistedVideos).to.be.an('array') | ||
148 | expect(blacklistedVideos.length).to.equal(2) | ||
149 | |||
150 | const result = [ ...body.data ].sort(sortObjectComparator('name', 'desc')) | ||
151 | expect(blacklistedVideos).to.deep.equal(result) | ||
152 | }) | ||
153 | |||
154 | it('Should get the correct sort when sorting by ascending creation date', async function () { | ||
155 | const body = await command.list({ sort: 'createdAt' }) | ||
156 | expect(body.total).to.equal(2) | ||
157 | |||
158 | const blacklistedVideos = body.data | ||
159 | expect(blacklistedVideos).to.be.an('array') | ||
160 | expect(blacklistedVideos.length).to.equal(2) | ||
161 | |||
162 | const result = [ ...body.data ].sort(sortObjectComparator('createdAt', 'asc')) | ||
163 | expect(blacklistedVideos).to.deep.equal(result) | ||
164 | }) | ||
165 | }) | ||
166 | |||
167 | describe('When updating blacklisted videos', function () { | ||
168 | it('Should change the reason', async function () { | ||
169 | await command.update({ videoId, reason: 'my super reason updated' }) | ||
170 | |||
171 | const body = await command.list({ sort: '-name' }) | ||
172 | const video = body.data.find(b => b.video.id === videoId) | ||
173 | |||
174 | expect(video.reason).to.equal('my super reason updated') | ||
175 | }) | ||
176 | }) | ||
177 | |||
178 | describe('When listing my videos', function () { | ||
179 | it('Should display blacklisted videos', async function () { | ||
180 | await blacklistVideosOnServer(servers[1]) | ||
181 | |||
182 | const { total, data } = await servers[1].videos.listMyVideos() | ||
183 | |||
184 | expect(total).to.equal(2) | ||
185 | expect(data).to.have.lengthOf(2) | ||
186 | |||
187 | for (const video of data) { | ||
188 | expect(video.blacklisted).to.be.true | ||
189 | expect(video.blacklistedReason).to.equal('super reason') | ||
190 | } | ||
191 | }) | ||
192 | }) | ||
193 | |||
194 | describe('When removing a blacklisted video', function () { | ||
195 | let videoToRemove: VideoBlacklist | ||
196 | let blacklist = [] | ||
197 | |||
198 | it('Should not have any video in videos list on server 1', async function () { | ||
199 | const { total, data } = await servers[0].videos.list() | ||
200 | expect(total).to.equal(0) | ||
201 | expect(data).to.be.an('array') | ||
202 | expect(data.length).to.equal(0) | ||
203 | }) | ||
204 | |||
205 | it('Should remove a video from the blacklist on server 1', async function () { | ||
206 | // Get one video in the blacklist | ||
207 | const body = await command.list({ sort: '-name' }) | ||
208 | videoToRemove = body.data[0] | ||
209 | blacklist = body.data.slice(1) | ||
210 | |||
211 | // Remove it | ||
212 | await command.remove({ videoId: videoToRemove.video.id }) | ||
213 | }) | ||
214 | |||
215 | it('Should have the ex-blacklisted video in videos list on server 1', async function () { | ||
216 | const { total, data } = await servers[0].videos.list() | ||
217 | expect(total).to.equal(1) | ||
218 | |||
219 | expect(data).to.be.an('array') | ||
220 | expect(data.length).to.equal(1) | ||
221 | |||
222 | expect(data[0].name).to.equal(videoToRemove.video.name) | ||
223 | expect(data[0].id).to.equal(videoToRemove.video.id) | ||
224 | }) | ||
225 | |||
226 | it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { | ||
227 | const body = await command.list({ sort: '-name' }) | ||
228 | expect(body.total).to.equal(1) | ||
229 | |||
230 | const videos = body.data | ||
231 | expect(videos).to.be.an('array') | ||
232 | expect(videos.length).to.equal(1) | ||
233 | expect(videos).to.deep.equal(blacklist) | ||
234 | }) | ||
235 | }) | ||
236 | |||
237 | describe('When blacklisting local videos', function () { | ||
238 | let video3UUID: string | ||
239 | let video4UUID: string | ||
240 | |||
241 | before(async function () { | ||
242 | { | ||
243 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'Video 3' } }) | ||
244 | video3UUID = uuid | ||
245 | } | ||
246 | { | ||
247 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'Video 4' } }) | ||
248 | video4UUID = uuid | ||
249 | } | ||
250 | |||
251 | await waitJobs(servers) | ||
252 | }) | ||
253 | |||
254 | it('Should blacklist video 3 and keep it federated', async function () { | ||
255 | await command.add({ videoId: video3UUID, reason: 'super reason', unfederate: false }) | ||
256 | |||
257 | await waitJobs(servers) | ||
258 | |||
259 | { | ||
260 | const { data } = await servers[0].videos.list() | ||
261 | expect(data.find(v => v.uuid === video3UUID)).to.be.undefined | ||
262 | } | ||
263 | |||
264 | { | ||
265 | const { data } = await servers[1].videos.list() | ||
266 | expect(data.find(v => v.uuid === video3UUID)).to.not.be.undefined | ||
267 | } | ||
268 | }) | ||
269 | |||
270 | it('Should unfederate the video', async function () { | ||
271 | await command.add({ videoId: video4UUID, reason: 'super reason', unfederate: true }) | ||
272 | |||
273 | await waitJobs(servers) | ||
274 | |||
275 | for (const server of servers) { | ||
276 | const { data } = await server.videos.list() | ||
277 | expect(data.find(v => v.uuid === video4UUID)).to.be.undefined | ||
278 | } | ||
279 | }) | ||
280 | |||
281 | it('Should have the video unfederated even after an Update AP message', async function () { | ||
282 | await servers[0].videos.update({ id: video4UUID, attributes: { description: 'super description' } }) | ||
283 | |||
284 | await waitJobs(servers) | ||
285 | |||
286 | for (const server of servers) { | ||
287 | const { data } = await server.videos.list() | ||
288 | expect(data.find(v => v.uuid === video4UUID)).to.be.undefined | ||
289 | } | ||
290 | }) | ||
291 | |||
292 | it('Should have the correct video blacklist unfederate attribute', async function () { | ||
293 | const body = await command.list({ sort: 'createdAt' }) | ||
294 | |||
295 | const blacklistedVideos = body.data | ||
296 | const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID) | ||
297 | const video4Blacklisted = blacklistedVideos.find(b => b.video.uuid === video4UUID) | ||
298 | |||
299 | expect(video3Blacklisted.unfederated).to.be.false | ||
300 | expect(video4Blacklisted.unfederated).to.be.true | ||
301 | }) | ||
302 | |||
303 | it('Should remove the video from blacklist and refederate the video', async function () { | ||
304 | await command.remove({ videoId: video4UUID }) | ||
305 | |||
306 | await waitJobs(servers) | ||
307 | |||
308 | for (const server of servers) { | ||
309 | const { data } = await server.videos.list() | ||
310 | expect(data.find(v => v.uuid === video4UUID)).to.not.be.undefined | ||
311 | } | ||
312 | }) | ||
313 | |||
314 | }) | ||
315 | |||
316 | describe('When auto blacklist videos', function () { | ||
317 | let userWithoutFlag: string | ||
318 | let userWithFlag: string | ||
319 | let channelOfUserWithoutFlag: number | ||
320 | |||
321 | before(async function () { | ||
322 | this.timeout(20000) | ||
323 | |||
324 | await killallServers([ servers[0] ]) | ||
325 | |||
326 | const config = { | ||
327 | auto_blacklist: { | ||
328 | videos: { | ||
329 | of_users: { | ||
330 | enabled: true | ||
331 | } | ||
332 | } | ||
333 | } | ||
334 | } | ||
335 | await servers[0].run(config) | ||
336 | |||
337 | { | ||
338 | const user = { username: 'user_without_flag', password: 'password' } | ||
339 | await servers[0].users.create({ | ||
340 | username: user.username, | ||
341 | adminFlags: UserAdminFlag.NONE, | ||
342 | password: user.password, | ||
343 | role: UserRole.USER | ||
344 | }) | ||
345 | |||
346 | userWithoutFlag = await servers[0].login.getAccessToken(user) | ||
347 | |||
348 | const { videoChannels } = await servers[0].users.getMyInfo({ token: userWithoutFlag }) | ||
349 | channelOfUserWithoutFlag = videoChannels[0].id | ||
350 | } | ||
351 | |||
352 | { | ||
353 | const user = { username: 'user_with_flag', password: 'password' } | ||
354 | await servers[0].users.create({ | ||
355 | username: user.username, | ||
356 | adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST, | ||
357 | password: user.password, | ||
358 | role: UserRole.USER | ||
359 | }) | ||
360 | |||
361 | userWithFlag = await servers[0].login.getAccessToken(user) | ||
362 | } | ||
363 | |||
364 | await waitJobs(servers) | ||
365 | }) | ||
366 | |||
367 | it('Should auto blacklist a video on upload', async function () { | ||
368 | await servers[0].videos.upload({ token: userWithoutFlag, attributes: { name: 'blacklisted' } }) | ||
369 | |||
370 | const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED }) | ||
371 | expect(body.total).to.equal(1) | ||
372 | expect(body.data[0].video.name).to.equal('blacklisted') | ||
373 | }) | ||
374 | |||
375 | it('Should auto blacklist a video on URL import', async function () { | ||
376 | this.timeout(15000) | ||
377 | |||
378 | const attributes = { | ||
379 | targetUrl: FIXTURE_URLS.goodVideo, | ||
380 | name: 'URL import', | ||
381 | channelId: channelOfUserWithoutFlag | ||
382 | } | ||
383 | await servers[0].imports.importVideo({ token: userWithoutFlag, attributes }) | ||
384 | |||
385 | const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED }) | ||
386 | expect(body.total).to.equal(2) | ||
387 | expect(body.data[1].video.name).to.equal('URL import') | ||
388 | }) | ||
389 | |||
390 | it('Should auto blacklist a video on torrent import', async function () { | ||
391 | const attributes = { | ||
392 | magnetUri: FIXTURE_URLS.magnet, | ||
393 | name: 'Torrent import', | ||
394 | channelId: channelOfUserWithoutFlag | ||
395 | } | ||
396 | await servers[0].imports.importVideo({ token: userWithoutFlag, attributes }) | ||
397 | |||
398 | const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED }) | ||
399 | expect(body.total).to.equal(3) | ||
400 | expect(body.data[2].video.name).to.equal('Torrent import') | ||
401 | }) | ||
402 | |||
403 | it('Should not auto blacklist a video on upload if the user has the bypass blacklist flag', async function () { | ||
404 | await servers[0].videos.upload({ token: userWithFlag, attributes: { name: 'not blacklisted' } }) | ||
405 | |||
406 | const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED }) | ||
407 | expect(body.total).to.equal(3) | ||
408 | }) | ||
409 | }) | ||
410 | |||
411 | after(async function () { | ||
412 | await cleanupTests(servers) | ||
413 | }) | ||
414 | }) | ||
diff --git a/packages/tests/src/api/notifications/admin-notifications.ts b/packages/tests/src/api/notifications/admin-notifications.ts new file mode 100644 index 000000000..2186dc55a --- /dev/null +++ b/packages/tests/src/api/notifications/admin-notifications.ts | |||
@@ -0,0 +1,154 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models' | ||
6 | import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
7 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
8 | import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js' | ||
9 | import { CheckerBaseParams, prepareNotificationsTest, checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications.js' | ||
10 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
11 | |||
12 | describe('Test admin notifications', function () { | ||
13 | let server: PeerTubeServer | ||
14 | let sqlCommand: SQLCommand | ||
15 | let userNotifications: UserNotification[] = [] | ||
16 | let adminNotifications: UserNotification[] = [] | ||
17 | let emails: object[] = [] | ||
18 | let baseParams: CheckerBaseParams | ||
19 | let joinPeerTubeServer: MockJoinPeerTubeVersions | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(120000) | ||
23 | |||
24 | joinPeerTubeServer = new MockJoinPeerTubeVersions() | ||
25 | const port = await joinPeerTubeServer.initialize() | ||
26 | |||
27 | const config = { | ||
28 | peertube: { | ||
29 | check_latest_version: { | ||
30 | enabled: true, | ||
31 | url: `http://127.0.0.1:${port}/versions.json` | ||
32 | } | ||
33 | }, | ||
34 | plugins: { | ||
35 | index: { | ||
36 | enabled: true, | ||
37 | check_latest_versions_interval: '3 seconds' | ||
38 | } | ||
39 | } | ||
40 | } | ||
41 | |||
42 | const res = await prepareNotificationsTest(1, config) | ||
43 | emails = res.emails | ||
44 | server = res.servers[0] | ||
45 | |||
46 | userNotifications = res.userNotifications | ||
47 | adminNotifications = res.adminNotifications | ||
48 | |||
49 | baseParams = { | ||
50 | server, | ||
51 | emails, | ||
52 | socketNotifications: adminNotifications, | ||
53 | token: server.accessToken | ||
54 | } | ||
55 | |||
56 | await server.plugins.install({ npmName: 'peertube-plugin-hello-world' }) | ||
57 | await server.plugins.install({ npmName: 'peertube-theme-background-red' }) | ||
58 | |||
59 | sqlCommand = new SQLCommand(server) | ||
60 | }) | ||
61 | |||
62 | describe('Latest PeerTube version notification', function () { | ||
63 | |||
64 | it('Should not send a notification to admins if there is no new version', async function () { | ||
65 | this.timeout(30000) | ||
66 | |||
67 | joinPeerTubeServer.setLatestVersion('1.4.2') | ||
68 | |||
69 | await wait(3000) | ||
70 | await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '1.4.2', checkType: 'absence' }) | ||
71 | }) | ||
72 | |||
73 | it('Should send a notification to admins on new version', async function () { | ||
74 | this.timeout(30000) | ||
75 | |||
76 | joinPeerTubeServer.setLatestVersion('15.4.2') | ||
77 | |||
78 | await wait(3000) | ||
79 | await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.2', checkType: 'presence' }) | ||
80 | }) | ||
81 | |||
82 | it('Should not send the same notification to admins', async function () { | ||
83 | this.timeout(30000) | ||
84 | |||
85 | await wait(3000) | ||
86 | expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1) | ||
87 | }) | ||
88 | |||
89 | it('Should not have sent a notification to users', async function () { | ||
90 | this.timeout(30000) | ||
91 | |||
92 | expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0) | ||
93 | }) | ||
94 | |||
95 | it('Should send a new notification after a new release', async function () { | ||
96 | this.timeout(30000) | ||
97 | |||
98 | joinPeerTubeServer.setLatestVersion('15.4.3') | ||
99 | |||
100 | await wait(3000) | ||
101 | await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.3', checkType: 'presence' }) | ||
102 | expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2) | ||
103 | }) | ||
104 | }) | ||
105 | |||
106 | describe('Latest plugin version notification', function () { | ||
107 | |||
108 | it('Should not send a notification to admins if there is no new plugin version', async function () { | ||
109 | this.timeout(30000) | ||
110 | |||
111 | await wait(6000) | ||
112 | await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'absence' }) | ||
113 | }) | ||
114 | |||
115 | it('Should send a notification to admins on new plugin version', async function () { | ||
116 | this.timeout(30000) | ||
117 | |||
118 | await sqlCommand.setPluginVersion('hello-world', '0.0.1') | ||
119 | await sqlCommand.setPluginLatestVersion('hello-world', '0.0.1') | ||
120 | await wait(6000) | ||
121 | |||
122 | await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'presence' }) | ||
123 | }) | ||
124 | |||
125 | it('Should not send the same notification to admins', async function () { | ||
126 | this.timeout(30000) | ||
127 | |||
128 | await wait(6000) | ||
129 | |||
130 | expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1) | ||
131 | }) | ||
132 | |||
133 | it('Should not have sent a notification to users', async function () { | ||
134 | expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0) | ||
135 | }) | ||
136 | |||
137 | it('Should send a new notification after a new plugin release', async function () { | ||
138 | this.timeout(30000) | ||
139 | |||
140 | await sqlCommand.setPluginVersion('hello-world', '0.0.1') | ||
141 | await sqlCommand.setPluginLatestVersion('hello-world', '0.0.1') | ||
142 | await wait(6000) | ||
143 | |||
144 | expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2) | ||
145 | }) | ||
146 | }) | ||
147 | |||
148 | after(async function () { | ||
149 | MockSmtpServer.Instance.kill() | ||
150 | |||
151 | await sqlCommand.cleanup() | ||
152 | await cleanupTests([ server ]) | ||
153 | }) | ||
154 | }) | ||
diff --git a/packages/tests/src/api/notifications/comments-notifications.ts b/packages/tests/src/api/notifications/comments-notifications.ts new file mode 100644 index 000000000..5647d1286 --- /dev/null +++ b/packages/tests/src/api/notifications/comments-notifications.ts | |||
@@ -0,0 +1,300 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { UserNotification } from '@peertube/peertube-models' | ||
5 | import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' | ||
6 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
7 | import { prepareNotificationsTest, CheckerBaseParams, checkNewCommentOnMyVideo, checkCommentMention } from '@tests/shared/notifications.js' | ||
8 | |||
9 | describe('Test comments notifications', function () { | ||
10 | let servers: PeerTubeServer[] = [] | ||
11 | let userToken: string | ||
12 | let userNotifications: UserNotification[] = [] | ||
13 | let emails: object[] = [] | ||
14 | |||
15 | const commentText = '**hello** <a href="https://joinpeertube.org">world</a>, <h1>what do you think about peertube?</h1>' | ||
16 | const expectedHtml = '<strong>hello</strong> <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">world</a>' + | ||
17 | ', </p>what do you think about peertube?' | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(120000) | ||
21 | |||
22 | const res = await prepareNotificationsTest(2) | ||
23 | emails = res.emails | ||
24 | userToken = res.userAccessToken | ||
25 | servers = res.servers | ||
26 | userNotifications = res.userNotifications | ||
27 | }) | ||
28 | |||
29 | describe('Comment on my video notifications', function () { | ||
30 | let baseParams: CheckerBaseParams | ||
31 | |||
32 | before(() => { | ||
33 | baseParams = { | ||
34 | server: servers[0], | ||
35 | emails, | ||
36 | socketNotifications: userNotifications, | ||
37 | token: userToken | ||
38 | } | ||
39 | }) | ||
40 | |||
41 | it('Should not send a new comment notification after a comment on another video', async function () { | ||
42 | this.timeout(30000) | ||
43 | |||
44 | const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) | ||
45 | |||
46 | const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' }) | ||
47 | const commentId = created.id | ||
48 | |||
49 | await waitJobs(servers) | ||
50 | await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' }) | ||
51 | }) | ||
52 | |||
53 | it('Should not send a new comment notification if I comment my own video', async function () { | ||
54 | this.timeout(30000) | ||
55 | |||
56 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) | ||
57 | |||
58 | const created = await servers[0].comments.createThread({ token: userToken, videoId: uuid, text: 'comment' }) | ||
59 | const commentId = created.id | ||
60 | |||
61 | await waitJobs(servers) | ||
62 | await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' }) | ||
63 | }) | ||
64 | |||
65 | it('Should not send a new comment notification if the account is muted', async function () { | ||
66 | this.timeout(30000) | ||
67 | |||
68 | await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' }) | ||
69 | |||
70 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) | ||
71 | |||
72 | const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' }) | ||
73 | const commentId = created.id | ||
74 | |||
75 | await waitJobs(servers) | ||
76 | await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' }) | ||
77 | |||
78 | await servers[0].blocklist.removeFromMyBlocklist({ token: userToken, account: 'root' }) | ||
79 | }) | ||
80 | |||
81 | it('Should send a new comment notification after a local comment on my video', async function () { | ||
82 | this.timeout(30000) | ||
83 | |||
84 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) | ||
85 | |||
86 | const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' }) | ||
87 | const commentId = created.id | ||
88 | |||
89 | await waitJobs(servers) | ||
90 | await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence' }) | ||
91 | }) | ||
92 | |||
93 | it('Should send a new comment notification after a remote comment on my video', async function () { | ||
94 | this.timeout(30000) | ||
95 | |||
96 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) | ||
97 | |||
98 | await waitJobs(servers) | ||
99 | |||
100 | await servers[1].comments.createThread({ videoId: uuid, text: 'comment' }) | ||
101 | |||
102 | await waitJobs(servers) | ||
103 | |||
104 | const { data } = await servers[0].comments.listThreads({ videoId: uuid }) | ||
105 | expect(data).to.have.lengthOf(1) | ||
106 | |||
107 | const commentId = data[0].id | ||
108 | await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence' }) | ||
109 | }) | ||
110 | |||
111 | it('Should send a new comment notification after a local reply on my video', async function () { | ||
112 | this.timeout(30000) | ||
113 | |||
114 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) | ||
115 | |||
116 | const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' }) | ||
117 | |||
118 | const { id: commentId } = await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'reply' }) | ||
119 | |||
120 | await waitJobs(servers) | ||
121 | await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId, commentId, checkType: 'presence' }) | ||
122 | }) | ||
123 | |||
124 | it('Should send a new comment notification after a remote reply on my video', async function () { | ||
125 | this.timeout(30000) | ||
126 | |||
127 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) | ||
128 | await waitJobs(servers) | ||
129 | |||
130 | { | ||
131 | const created = await servers[1].comments.createThread({ videoId: uuid, text: 'comment' }) | ||
132 | const threadId = created.id | ||
133 | await servers[1].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'reply' }) | ||
134 | } | ||
135 | |||
136 | await waitJobs(servers) | ||
137 | |||
138 | const { data } = await servers[0].comments.listThreads({ videoId: uuid }) | ||
139 | expect(data).to.have.lengthOf(1) | ||
140 | |||
141 | const threadId = data[0].id | ||
142 | const tree = await servers[0].comments.getThread({ videoId: uuid, threadId }) | ||
143 | |||
144 | expect(tree.children).to.have.lengthOf(1) | ||
145 | const commentId = tree.children[0].comment.id | ||
146 | |||
147 | await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId, commentId, checkType: 'presence' }) | ||
148 | }) | ||
149 | |||
150 | it('Should convert markdown in comment to html', async function () { | ||
151 | this.timeout(30000) | ||
152 | |||
153 | const { uuid } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'cool video' } }) | ||
154 | |||
155 | await servers[0].comments.createThread({ videoId: uuid, text: commentText }) | ||
156 | |||
157 | await waitJobs(servers) | ||
158 | |||
159 | const latestEmail = emails[emails.length - 1] | ||
160 | expect(latestEmail['html']).to.contain(expectedHtml) | ||
161 | }) | ||
162 | }) | ||
163 | |||
164 | describe('Mention notifications', function () { | ||
165 | let baseParams: CheckerBaseParams | ||
166 | const byAccountDisplayName = 'super root name' | ||
167 | |||
168 | before(async function () { | ||
169 | baseParams = { | ||
170 | server: servers[0], | ||
171 | emails, | ||
172 | socketNotifications: userNotifications, | ||
173 | token: userToken | ||
174 | } | ||
175 | |||
176 | await servers[0].users.updateMe({ displayName: 'super root name' }) | ||
177 | await servers[1].users.updateMe({ displayName: 'super root 2 name' }) | ||
178 | }) | ||
179 | |||
180 | it('Should not send a new mention comment notification if I mention the video owner', async function () { | ||
181 | this.timeout(30000) | ||
182 | |||
183 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) | ||
184 | |||
185 | const { id: commentId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello' }) | ||
186 | |||
187 | await waitJobs(servers) | ||
188 | await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' }) | ||
189 | }) | ||
190 | |||
191 | it('Should not send a new mention comment notification if I mention myself', async function () { | ||
192 | this.timeout(30000) | ||
193 | |||
194 | const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) | ||
195 | |||
196 | const { id: commentId } = await servers[0].comments.createThread({ token: userToken, videoId: uuid, text: '@user_1 hello' }) | ||
197 | |||
198 | await waitJobs(servers) | ||
199 | await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' }) | ||
200 | }) | ||
201 | |||
202 | it('Should not send a new mention notification if the account is muted', async function () { | ||
203 | this.timeout(30000) | ||
204 | |||
205 | await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' }) | ||
206 | |||
207 | const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) | ||
208 | |||
209 | const { id: commentId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello' }) | ||
210 | |||
211 | await waitJobs(servers) | ||
212 | await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' }) | ||
213 | |||
214 | await servers[0].blocklist.removeFromMyBlocklist({ token: userToken, account: 'root' }) | ||
215 | }) | ||
216 | |||
217 | it('Should not send a new mention notification if the remote account mention a local account', async function () { | ||
218 | this.timeout(30000) | ||
219 | |||
220 | const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) | ||
221 | |||
222 | await waitJobs(servers) | ||
223 | const { id: threadId } = await servers[1].comments.createThread({ videoId: uuid, text: '@user_1 hello' }) | ||
224 | |||
225 | await waitJobs(servers) | ||
226 | |||
227 | const byAccountDisplayName = 'super root 2 name' | ||
228 | await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'absence' }) | ||
229 | }) | ||
230 | |||
231 | it('Should send a new mention notification after local comments', async function () { | ||
232 | this.timeout(30000) | ||
233 | |||
234 | const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) | ||
235 | |||
236 | const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hellotext: 1' }) | ||
237 | |||
238 | await waitJobs(servers) | ||
239 | await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'presence' }) | ||
240 | |||
241 | const { id: commentId } = await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'hello 2 @user_1' }) | ||
242 | |||
243 | await waitJobs(servers) | ||
244 | await checkCommentMention({ ...baseParams, shortUUID, commentId, threadId, byAccountDisplayName, checkType: 'presence' }) | ||
245 | }) | ||
246 | |||
247 | it('Should send a new mention notification after remote comments', async function () { | ||
248 | this.timeout(30000) | ||
249 | |||
250 | const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) | ||
251 | |||
252 | await waitJobs(servers) | ||
253 | |||
254 | const text1 = `hello @user_1@${servers[0].host} 1` | ||
255 | const { id: server2ThreadId } = await servers[1].comments.createThread({ videoId: uuid, text: text1 }) | ||
256 | |||
257 | await waitJobs(servers) | ||
258 | |||
259 | const { data } = await servers[0].comments.listThreads({ videoId: uuid }) | ||
260 | expect(data).to.have.lengthOf(1) | ||
261 | |||
262 | const byAccountDisplayName = 'super root 2 name' | ||
263 | const threadId = data[0].id | ||
264 | await checkCommentMention({ ...baseParams, shortUUID, commentId: threadId, threadId, byAccountDisplayName, checkType: 'presence' }) | ||
265 | |||
266 | const text2 = `@user_1@${servers[0].host} hello 2 @root@${servers[0].host}` | ||
267 | await servers[1].comments.addReply({ videoId: uuid, toCommentId: server2ThreadId, text: text2 }) | ||
268 | |||
269 | await waitJobs(servers) | ||
270 | |||
271 | const tree = await servers[0].comments.getThread({ videoId: uuid, threadId }) | ||
272 | |||
273 | expect(tree.children).to.have.lengthOf(1) | ||
274 | const commentId = tree.children[0].comment.id | ||
275 | |||
276 | await checkCommentMention({ ...baseParams, shortUUID, commentId, threadId, byAccountDisplayName, checkType: 'presence' }) | ||
277 | }) | ||
278 | |||
279 | it('Should convert markdown in comment to html', async function () { | ||
280 | this.timeout(30000) | ||
281 | |||
282 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) | ||
283 | |||
284 | const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello 1' }) | ||
285 | |||
286 | await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: '@user_1 ' + commentText }) | ||
287 | |||
288 | await waitJobs(servers) | ||
289 | |||
290 | const latestEmail = emails[emails.length - 1] | ||
291 | expect(latestEmail['html']).to.contain(expectedHtml) | ||
292 | }) | ||
293 | }) | ||
294 | |||
295 | after(async function () { | ||
296 | MockSmtpServer.Instance.kill() | ||
297 | |||
298 | await cleanupTests(servers) | ||
299 | }) | ||
300 | }) | ||
diff --git a/packages/tests/src/api/notifications/index.ts b/packages/tests/src/api/notifications/index.ts new file mode 100644 index 000000000..d63d94182 --- /dev/null +++ b/packages/tests/src/api/notifications/index.ts | |||
@@ -0,0 +1,6 @@ | |||
1 | import './admin-notifications.js' | ||
2 | import './comments-notifications.js' | ||
3 | import './moderation-notifications.js' | ||
4 | import './notifications-api.js' | ||
5 | import './registrations-notifications.js' | ||
6 | import './user-notifications.js' | ||
diff --git a/packages/tests/src/api/notifications/moderation-notifications.ts b/packages/tests/src/api/notifications/moderation-notifications.ts new file mode 100644 index 000000000..493764882 --- /dev/null +++ b/packages/tests/src/api/notifications/moderation-notifications.ts | |||
@@ -0,0 +1,609 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { AbuseState, CustomConfig, UserNotification, UserRole, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
6 | import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' | ||
7 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
8 | import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js' | ||
9 | import { | ||
10 | prepareNotificationsTest, | ||
11 | CheckerBaseParams, | ||
12 | checkNewVideoAbuseForModerators, | ||
13 | checkNewCommentAbuseForModerators, | ||
14 | checkNewAccountAbuseForModerators, | ||
15 | checkAbuseStateChange, | ||
16 | checkNewAbuseMessage, | ||
17 | checkNewBlacklistOnMyVideo, | ||
18 | checkNewInstanceFollower, | ||
19 | checkAutoInstanceFollowing, | ||
20 | checkVideoAutoBlacklistForModerators, | ||
21 | checkVideoIsPublished, | ||
22 | checkNewVideoFromSubscription | ||
23 | } from '@tests/shared/notifications.js' | ||
24 | |||
25 | describe('Test moderation notifications', function () { | ||
26 | let servers: PeerTubeServer[] = [] | ||
27 | let userToken1: string | ||
28 | let userToken2: string | ||
29 | |||
30 | let userNotifications: UserNotification[] = [] | ||
31 | let adminNotifications: UserNotification[] = [] | ||
32 | let adminNotificationsServer2: UserNotification[] = [] | ||
33 | let emails: object[] = [] | ||
34 | |||
35 | before(async function () { | ||
36 | this.timeout(120000) | ||
37 | |||
38 | const res = await prepareNotificationsTest(3) | ||
39 | emails = res.emails | ||
40 | userToken1 = res.userAccessToken | ||
41 | servers = res.servers | ||
42 | userNotifications = res.userNotifications | ||
43 | adminNotifications = res.adminNotifications | ||
44 | adminNotificationsServer2 = res.adminNotificationsServer2 | ||
45 | |||
46 | userToken2 = await servers[1].users.generateUserAndToken('user2', UserRole.USER) | ||
47 | }) | ||
48 | |||
49 | describe('Abuse for moderators notification', function () { | ||
50 | let baseParams: CheckerBaseParams | ||
51 | |||
52 | before(() => { | ||
53 | baseParams = { | ||
54 | server: servers[0], | ||
55 | emails, | ||
56 | socketNotifications: adminNotifications, | ||
57 | token: servers[0].accessToken | ||
58 | } | ||
59 | }) | ||
60 | |||
61 | it('Should not send a notification to moderators on local abuse reported by an admin', async function () { | ||
62 | this.timeout(50000) | ||
63 | |||
64 | const name = 'video for abuse ' + buildUUID() | ||
65 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
66 | |||
67 | await servers[0].abuses.report({ videoId: video.id, reason: 'super reason' }) | ||
68 | |||
69 | await waitJobs(servers) | ||
70 | await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'absence' }) | ||
71 | }) | ||
72 | |||
73 | it('Should send a notification to moderators on local video abuse', async function () { | ||
74 | this.timeout(50000) | ||
75 | |||
76 | const name = 'video for abuse ' + buildUUID() | ||
77 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
78 | |||
79 | await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' }) | ||
80 | |||
81 | await waitJobs(servers) | ||
82 | await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' }) | ||
83 | }) | ||
84 | |||
85 | it('Should send a notification to moderators on remote video abuse', async function () { | ||
86 | this.timeout(50000) | ||
87 | |||
88 | const name = 'video for abuse ' + buildUUID() | ||
89 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
90 | |||
91 | await waitJobs(servers) | ||
92 | |||
93 | const videoId = await servers[1].videos.getId({ uuid: video.uuid }) | ||
94 | await servers[1].abuses.report({ token: userToken2, videoId, reason: 'super reason' }) | ||
95 | |||
96 | await waitJobs(servers) | ||
97 | await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' }) | ||
98 | }) | ||
99 | |||
100 | it('Should send a notification to moderators on local comment abuse', async function () { | ||
101 | this.timeout(50000) | ||
102 | |||
103 | const name = 'video for abuse ' + buildUUID() | ||
104 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
105 | const comment = await servers[0].comments.createThread({ | ||
106 | token: userToken1, | ||
107 | videoId: video.id, | ||
108 | text: 'comment abuse ' + buildUUID() | ||
109 | }) | ||
110 | |||
111 | await waitJobs(servers) | ||
112 | |||
113 | await servers[0].abuses.report({ token: userToken1, commentId: comment.id, reason: 'super reason' }) | ||
114 | |||
115 | await waitJobs(servers) | ||
116 | await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' }) | ||
117 | }) | ||
118 | |||
119 | it('Should send a notification to moderators on remote comment abuse', async function () { | ||
120 | this.timeout(50000) | ||
121 | |||
122 | const name = 'video for abuse ' + buildUUID() | ||
123 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
124 | |||
125 | await servers[0].comments.createThread({ | ||
126 | token: userToken1, | ||
127 | videoId: video.id, | ||
128 | text: 'comment abuse ' + buildUUID() | ||
129 | }) | ||
130 | |||
131 | await waitJobs(servers) | ||
132 | |||
133 | const { data } = await servers[1].comments.listThreads({ videoId: video.uuid }) | ||
134 | const commentId = data[0].id | ||
135 | await servers[1].abuses.report({ token: userToken2, commentId, reason: 'super reason' }) | ||
136 | |||
137 | await waitJobs(servers) | ||
138 | await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' }) | ||
139 | }) | ||
140 | |||
141 | it('Should send a notification to moderators on local account abuse', async function () { | ||
142 | this.timeout(50000) | ||
143 | |||
144 | const username = 'user' + new Date().getTime() | ||
145 | const { account } = await servers[0].users.create({ username, password: 'donald' }) | ||
146 | const accountId = account.id | ||
147 | |||
148 | await servers[0].abuses.report({ token: userToken1, accountId, reason: 'super reason' }) | ||
149 | |||
150 | await waitJobs(servers) | ||
151 | await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' }) | ||
152 | }) | ||
153 | |||
154 | it('Should send a notification to moderators on remote account abuse', async function () { | ||
155 | this.timeout(50000) | ||
156 | |||
157 | const username = 'user' + new Date().getTime() | ||
158 | const tmpToken = await servers[0].users.generateUserAndToken(username) | ||
159 | await servers[0].videos.upload({ token: tmpToken, attributes: { name: 'super video' } }) | ||
160 | |||
161 | await waitJobs(servers) | ||
162 | |||
163 | const account = await servers[1].accounts.get({ accountName: username + '@' + servers[0].host }) | ||
164 | await servers[1].abuses.report({ token: userToken2, accountId: account.id, reason: 'super reason' }) | ||
165 | |||
166 | await waitJobs(servers) | ||
167 | await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' }) | ||
168 | }) | ||
169 | }) | ||
170 | |||
171 | describe('Abuse state change notification', function () { | ||
172 | let baseParams: CheckerBaseParams | ||
173 | let abuseId: number | ||
174 | |||
175 | before(async function () { | ||
176 | baseParams = { | ||
177 | server: servers[0], | ||
178 | emails, | ||
179 | socketNotifications: userNotifications, | ||
180 | token: userToken1 | ||
181 | } | ||
182 | |||
183 | const name = 'abuse ' + buildUUID() | ||
184 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
185 | |||
186 | const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' }) | ||
187 | abuseId = body.abuse.id | ||
188 | }) | ||
189 | |||
190 | it('Should send a notification to reporter if the abuse has been accepted', async function () { | ||
191 | this.timeout(30000) | ||
192 | |||
193 | await servers[0].abuses.update({ abuseId, body: { state: AbuseState.ACCEPTED } }) | ||
194 | await waitJobs(servers) | ||
195 | |||
196 | await checkAbuseStateChange({ ...baseParams, abuseId, state: AbuseState.ACCEPTED, checkType: 'presence' }) | ||
197 | }) | ||
198 | |||
199 | it('Should send a notification to reporter if the abuse has been rejected', async function () { | ||
200 | this.timeout(30000) | ||
201 | |||
202 | await servers[0].abuses.update({ abuseId, body: { state: AbuseState.REJECTED } }) | ||
203 | await waitJobs(servers) | ||
204 | |||
205 | await checkAbuseStateChange({ ...baseParams, abuseId, state: AbuseState.REJECTED, checkType: 'presence' }) | ||
206 | }) | ||
207 | }) | ||
208 | |||
209 | describe('New abuse message notification', function () { | ||
210 | let baseParamsUser: CheckerBaseParams | ||
211 | let baseParamsAdmin: CheckerBaseParams | ||
212 | let abuseId: number | ||
213 | let abuseId2: number | ||
214 | |||
215 | before(async function () { | ||
216 | baseParamsUser = { | ||
217 | server: servers[0], | ||
218 | emails, | ||
219 | socketNotifications: userNotifications, | ||
220 | token: userToken1 | ||
221 | } | ||
222 | |||
223 | baseParamsAdmin = { | ||
224 | server: servers[0], | ||
225 | emails, | ||
226 | socketNotifications: adminNotifications, | ||
227 | token: servers[0].accessToken | ||
228 | } | ||
229 | |||
230 | const name = 'abuse ' + buildUUID() | ||
231 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
232 | |||
233 | { | ||
234 | const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' }) | ||
235 | abuseId = body.abuse.id | ||
236 | } | ||
237 | |||
238 | { | ||
239 | const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason 2' }) | ||
240 | abuseId2 = body.abuse.id | ||
241 | } | ||
242 | }) | ||
243 | |||
244 | it('Should send a notification to reporter on new message', async function () { | ||
245 | this.timeout(30000) | ||
246 | |||
247 | const message = 'my super message to users' | ||
248 | await servers[0].abuses.addMessage({ abuseId, message }) | ||
249 | await waitJobs(servers) | ||
250 | |||
251 | await checkNewAbuseMessage({ ...baseParamsUser, abuseId, message, toEmail: 'user_1@example.com', checkType: 'presence' }) | ||
252 | }) | ||
253 | |||
254 | it('Should not send a notification to the admin if sent by the admin', async function () { | ||
255 | this.timeout(30000) | ||
256 | |||
257 | const message = 'my super message that should not be sent to the admin' | ||
258 | await servers[0].abuses.addMessage({ abuseId, message }) | ||
259 | await waitJobs(servers) | ||
260 | |||
261 | const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com' | ||
262 | await checkNewAbuseMessage({ ...baseParamsAdmin, abuseId, message, toEmail, checkType: 'absence' }) | ||
263 | }) | ||
264 | |||
265 | it('Should send a notification to moderators', async function () { | ||
266 | this.timeout(30000) | ||
267 | |||
268 | const message = 'my super message to moderators' | ||
269 | await servers[0].abuses.addMessage({ token: userToken1, abuseId: abuseId2, message }) | ||
270 | await waitJobs(servers) | ||
271 | |||
272 | const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com' | ||
273 | await checkNewAbuseMessage({ ...baseParamsAdmin, abuseId: abuseId2, message, toEmail, checkType: 'presence' }) | ||
274 | }) | ||
275 | |||
276 | it('Should not send a notification to reporter if sent by the reporter', async function () { | ||
277 | this.timeout(30000) | ||
278 | |||
279 | const message = 'my super message that should not be sent to reporter' | ||
280 | await servers[0].abuses.addMessage({ token: userToken1, abuseId: abuseId2, message }) | ||
281 | await waitJobs(servers) | ||
282 | |||
283 | const toEmail = 'user_1@example.com' | ||
284 | await checkNewAbuseMessage({ ...baseParamsUser, abuseId: abuseId2, message, toEmail, checkType: 'absence' }) | ||
285 | }) | ||
286 | }) | ||
287 | |||
288 | describe('Video blacklist on my video', function () { | ||
289 | let baseParams: CheckerBaseParams | ||
290 | |||
291 | before(() => { | ||
292 | baseParams = { | ||
293 | server: servers[0], | ||
294 | emails, | ||
295 | socketNotifications: userNotifications, | ||
296 | token: userToken1 | ||
297 | } | ||
298 | }) | ||
299 | |||
300 | it('Should send a notification to video owner on blacklist', async function () { | ||
301 | this.timeout(30000) | ||
302 | |||
303 | const name = 'video for abuse ' + buildUUID() | ||
304 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
305 | |||
306 | await servers[0].blacklist.add({ videoId: uuid }) | ||
307 | |||
308 | await waitJobs(servers) | ||
309 | await checkNewBlacklistOnMyVideo({ ...baseParams, shortUUID, videoName: name, blacklistType: 'blacklist' }) | ||
310 | }) | ||
311 | |||
312 | it('Should send a notification to video owner on unblacklist', async function () { | ||
313 | this.timeout(30000) | ||
314 | |||
315 | const name = 'video for abuse ' + buildUUID() | ||
316 | const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | ||
317 | |||
318 | await servers[0].blacklist.add({ videoId: uuid }) | ||
319 | |||
320 | await waitJobs(servers) | ||
321 | await servers[0].blacklist.remove({ videoId: uuid }) | ||
322 | await waitJobs(servers) | ||
323 | |||
324 | await wait(500) | ||
325 | await checkNewBlacklistOnMyVideo({ ...baseParams, shortUUID, videoName: name, blacklistType: 'unblacklist' }) | ||
326 | }) | ||
327 | }) | ||
328 | |||
329 | describe('New instance follows', function () { | ||
330 | const instanceIndexServer = new MockInstancesIndex() | ||
331 | let config: any | ||
332 | let baseParams: CheckerBaseParams | ||
333 | |||
334 | before(async function () { | ||
335 | baseParams = { | ||
336 | server: servers[0], | ||
337 | emails, | ||
338 | socketNotifications: adminNotifications, | ||
339 | token: servers[0].accessToken | ||
340 | } | ||
341 | |||
342 | const port = await instanceIndexServer.initialize() | ||
343 | instanceIndexServer.addInstance(servers[1].host) | ||
344 | |||
345 | config = { | ||
346 | followings: { | ||
347 | instance: { | ||
348 | autoFollowIndex: { | ||
349 | indexUrl: `http://127.0.0.1:${port}/api/v1/instances/hosts`, | ||
350 | enabled: true | ||
351 | } | ||
352 | } | ||
353 | } | ||
354 | } | ||
355 | }) | ||
356 | |||
357 | it('Should send a notification only to admin when there is a new instance follower', async function () { | ||
358 | this.timeout(60000) | ||
359 | |||
360 | await servers[2].follows.follow({ hosts: [ servers[0].url ] }) | ||
361 | |||
362 | await waitJobs(servers) | ||
363 | |||
364 | await checkNewInstanceFollower({ ...baseParams, followerHost: servers[2].host, checkType: 'presence' }) | ||
365 | |||
366 | const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } } | ||
367 | await checkNewInstanceFollower({ ...baseParams, ...userOverride, followerHost: servers[2].host, checkType: 'absence' }) | ||
368 | }) | ||
369 | |||
370 | it('Should send a notification on auto follow back', async function () { | ||
371 | this.timeout(40000) | ||
372 | |||
373 | await servers[2].follows.unfollow({ target: servers[0] }) | ||
374 | await waitJobs(servers) | ||
375 | |||
376 | const config = { | ||
377 | followings: { | ||
378 | instance: { | ||
379 | autoFollowBack: { enabled: true } | ||
380 | } | ||
381 | } | ||
382 | } | ||
383 | await servers[0].config.updateCustomSubConfig({ newConfig: config }) | ||
384 | |||
385 | await servers[2].follows.follow({ hosts: [ servers[0].url ] }) | ||
386 | |||
387 | await waitJobs(servers) | ||
388 | |||
389 | const followerHost = servers[0].host | ||
390 | const followingHost = servers[2].host | ||
391 | await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' }) | ||
392 | |||
393 | const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } } | ||
394 | await checkAutoInstanceFollowing({ ...baseParams, ...userOverride, followerHost, followingHost, checkType: 'absence' }) | ||
395 | |||
396 | config.followings.instance.autoFollowBack.enabled = false | ||
397 | await servers[0].config.updateCustomSubConfig({ newConfig: config }) | ||
398 | await servers[0].follows.unfollow({ target: servers[2] }) | ||
399 | await servers[2].follows.unfollow({ target: servers[0] }) | ||
400 | }) | ||
401 | |||
402 | it('Should send a notification on auto instances index follow', async function () { | ||
403 | this.timeout(30000) | ||
404 | await servers[0].follows.unfollow({ target: servers[1] }) | ||
405 | |||
406 | await servers[0].config.updateCustomSubConfig({ newConfig: config }) | ||
407 | |||
408 | await wait(5000) | ||
409 | await waitJobs(servers) | ||
410 | |||
411 | const followerHost = servers[0].host | ||
412 | const followingHost = servers[1].host | ||
413 | await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' }) | ||
414 | |||
415 | config.followings.instance.autoFollowIndex.enabled = false | ||
416 | await servers[0].config.updateCustomSubConfig({ newConfig: config }) | ||
417 | await servers[0].follows.unfollow({ target: servers[1] }) | ||
418 | }) | ||
419 | }) | ||
420 | |||
421 | describe('Video-related notifications when video auto-blacklist is enabled', function () { | ||
422 | let userBaseParams: CheckerBaseParams | ||
423 | let adminBaseParamsServer1: CheckerBaseParams | ||
424 | let adminBaseParamsServer2: CheckerBaseParams | ||
425 | let uuid: string | ||
426 | let shortUUID: string | ||
427 | let videoName: string | ||
428 | let currentCustomConfig: CustomConfig | ||
429 | |||
430 | before(async function () { | ||
431 | |||
432 | adminBaseParamsServer1 = { | ||
433 | server: servers[0], | ||
434 | emails, | ||
435 | socketNotifications: adminNotifications, | ||
436 | token: servers[0].accessToken | ||
437 | } | ||
438 | |||
439 | adminBaseParamsServer2 = { | ||
440 | server: servers[1], | ||
441 | emails, | ||
442 | socketNotifications: adminNotificationsServer2, | ||
443 | token: servers[1].accessToken | ||
444 | } | ||
445 | |||
446 | userBaseParams = { | ||
447 | server: servers[0], | ||
448 | emails, | ||
449 | socketNotifications: userNotifications, | ||
450 | token: userToken1 | ||
451 | } | ||
452 | |||
453 | currentCustomConfig = await servers[0].config.getCustomConfig() | ||
454 | |||
455 | const autoBlacklistTestsCustomConfig = { | ||
456 | ...currentCustomConfig, | ||
457 | |||
458 | autoBlacklist: { | ||
459 | videos: { | ||
460 | ofUsers: { | ||
461 | enabled: true | ||
462 | } | ||
463 | } | ||
464 | } | ||
465 | } | ||
466 | |||
467 | // enable transcoding otherwise own publish notification after transcoding not expected | ||
468 | autoBlacklistTestsCustomConfig.transcoding.enabled = true | ||
469 | await servers[0].config.updateCustomConfig({ newCustomConfig: autoBlacklistTestsCustomConfig }) | ||
470 | |||
471 | await servers[0].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host }) | ||
472 | await servers[1].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host }) | ||
473 | }) | ||
474 | |||
475 | it('Should send notification to moderators on new video with auto-blacklist', async function () { | ||
476 | this.timeout(120000) | ||
477 | |||
478 | videoName = 'video with auto-blacklist ' + buildUUID() | ||
479 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name: videoName } }) | ||
480 | shortUUID = video.shortUUID | ||
481 | uuid = video.uuid | ||
482 | |||
483 | await waitJobs(servers) | ||
484 | await checkVideoAutoBlacklistForModerators({ ...adminBaseParamsServer1, shortUUID, videoName, checkType: 'presence' }) | ||
485 | }) | ||
486 | |||
487 | it('Should not send video publish notification if auto-blacklisted', async function () { | ||
488 | this.timeout(120000) | ||
489 | |||
490 | await checkVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' }) | ||
491 | }) | ||
492 | |||
493 | it('Should not send a local user subscription notification if auto-blacklisted', async function () { | ||
494 | this.timeout(120000) | ||
495 | |||
496 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'absence' }) | ||
497 | }) | ||
498 | |||
499 | it('Should not send a remote user subscription notification if auto-blacklisted', async function () { | ||
500 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'absence' }) | ||
501 | }) | ||
502 | |||
503 | it('Should send video published and unblacklist after video unblacklisted', async function () { | ||
504 | this.timeout(120000) | ||
505 | |||
506 | await servers[0].blacklist.remove({ videoId: uuid }) | ||
507 | |||
508 | await waitJobs(servers) | ||
509 | |||
510 | // FIXME: Can't test as two notifications sent to same user and util only checks last one | ||
511 | // One notification might be better anyways | ||
512 | // await checkNewBlacklistOnMyVideo(userBaseParams, videoUUID, videoName, 'unblacklist') | ||
513 | // await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'presence') | ||
514 | }) | ||
515 | |||
516 | it('Should send a local user subscription notification after removed from blacklist', async function () { | ||
517 | this.timeout(120000) | ||
518 | |||
519 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'presence' }) | ||
520 | }) | ||
521 | |||
522 | it('Should send a remote user subscription notification after removed from blacklist', async function () { | ||
523 | this.timeout(120000) | ||
524 | |||
525 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'presence' }) | ||
526 | }) | ||
527 | |||
528 | it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () { | ||
529 | this.timeout(120000) | ||
530 | |||
531 | const updateAt = new Date(new Date().getTime() + 1000000) | ||
532 | |||
533 | const name = 'video with auto-blacklist and future schedule ' + buildUUID() | ||
534 | |||
535 | const attributes = { | ||
536 | name, | ||
537 | privacy: VideoPrivacy.PRIVATE, | ||
538 | scheduleUpdate: { | ||
539 | updateAt: updateAt.toISOString(), | ||
540 | privacy: VideoPrivacy.PUBLIC | ||
541 | } | ||
542 | } | ||
543 | |||
544 | const { shortUUID, uuid } = await servers[0].videos.upload({ token: userToken1, attributes }) | ||
545 | |||
546 | await servers[0].blacklist.remove({ videoId: uuid }) | ||
547 | |||
548 | await waitJobs(servers) | ||
549 | await checkNewBlacklistOnMyVideo({ ...userBaseParams, shortUUID, videoName: name, blacklistType: 'unblacklist' }) | ||
550 | |||
551 | // FIXME: Can't test absence as two notifications sent to same user and util only checks last one | ||
552 | // One notification might be better anyways | ||
553 | // await checkVideoIsPublished(userBaseParams, name, uuid, 'absence') | ||
554 | |||
555 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' }) | ||
556 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' }) | ||
557 | }) | ||
558 | |||
559 | it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () { | ||
560 | this.timeout(120000) | ||
561 | |||
562 | // In 2 seconds | ||
563 | const updateAt = new Date(new Date().getTime() + 2000) | ||
564 | |||
565 | const name = 'video with schedule done and still auto-blacklisted ' + buildUUID() | ||
566 | |||
567 | const attributes = { | ||
568 | name, | ||
569 | privacy: VideoPrivacy.PRIVATE, | ||
570 | scheduleUpdate: { | ||
571 | updateAt: updateAt.toISOString(), | ||
572 | privacy: VideoPrivacy.PUBLIC | ||
573 | } | ||
574 | } | ||
575 | |||
576 | const { shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes }) | ||
577 | |||
578 | await wait(6000) | ||
579 | await checkVideoIsPublished({ ...userBaseParams, videoName: name, shortUUID, checkType: 'absence' }) | ||
580 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' }) | ||
581 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' }) | ||
582 | }) | ||
583 | |||
584 | it('Should not send a notification to moderators on new video without auto-blacklist', async function () { | ||
585 | this.timeout(120000) | ||
586 | |||
587 | const name = 'video without auto-blacklist ' + buildUUID() | ||
588 | |||
589 | // admin with blacklist right will not be auto-blacklisted | ||
590 | const { shortUUID } = await servers[0].videos.upload({ attributes: { name } }) | ||
591 | |||
592 | await waitJobs(servers) | ||
593 | await checkVideoAutoBlacklistForModerators({ ...adminBaseParamsServer1, shortUUID, videoName: name, checkType: 'absence' }) | ||
594 | }) | ||
595 | |||
596 | after(async () => { | ||
597 | await servers[0].config.updateCustomConfig({ newCustomConfig: currentCustomConfig }) | ||
598 | |||
599 | await servers[0].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host }) | ||
600 | await servers[1].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host }) | ||
601 | }) | ||
602 | }) | ||
603 | |||
604 | after(async function () { | ||
605 | MockSmtpServer.Instance.kill() | ||
606 | |||
607 | await cleanupTests(servers) | ||
608 | }) | ||
609 | }) | ||
diff --git a/packages/tests/src/api/notifications/notifications-api.ts b/packages/tests/src/api/notifications/notifications-api.ts new file mode 100644 index 000000000..1c7461553 --- /dev/null +++ b/packages/tests/src/api/notifications/notifications-api.ts | |||
@@ -0,0 +1,206 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { UserNotification, UserNotificationSettingValue } from '@peertube/peertube-models' | ||
5 | import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' | ||
6 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
7 | import { | ||
8 | prepareNotificationsTest, | ||
9 | CheckerBaseParams, | ||
10 | getAllNotificationsSettings, | ||
11 | checkNewVideoFromSubscription | ||
12 | } from '@tests/shared/notifications.js' | ||
13 | |||
14 | describe('Test notifications API', function () { | ||
15 | let server: PeerTubeServer | ||
16 | let userNotifications: UserNotification[] = [] | ||
17 | let userToken: string | ||
18 | let emails: object[] = [] | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(120000) | ||
22 | |||
23 | const res = await prepareNotificationsTest(1) | ||
24 | emails = res.emails | ||
25 | userToken = res.userAccessToken | ||
26 | userNotifications = res.userNotifications | ||
27 | server = res.servers[0] | ||
28 | |||
29 | await server.subscriptions.add({ token: userToken, targetUri: 'root_channel@' + server.host }) | ||
30 | |||
31 | for (let i = 0; i < 10; i++) { | ||
32 | await server.videos.randomUpload({ wait: false }) | ||
33 | } | ||
34 | |||
35 | await waitJobs([ server ]) | ||
36 | }) | ||
37 | |||
38 | describe('Notification list & count', function () { | ||
39 | |||
40 | it('Should correctly list notifications', async function () { | ||
41 | const { data, total } = await server.notifications.list({ token: userToken, start: 0, count: 2 }) | ||
42 | |||
43 | expect(data).to.have.lengthOf(2) | ||
44 | expect(total).to.equal(10) | ||
45 | }) | ||
46 | }) | ||
47 | |||
48 | describe('Mark as read', function () { | ||
49 | |||
50 | it('Should mark as read some notifications', async function () { | ||
51 | const { data } = await server.notifications.list({ token: userToken, start: 2, count: 3 }) | ||
52 | const ids = data.map(n => n.id) | ||
53 | |||
54 | await server.notifications.markAsRead({ token: userToken, ids }) | ||
55 | }) | ||
56 | |||
57 | it('Should have the notifications marked as read', async function () { | ||
58 | const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10 }) | ||
59 | |||
60 | expect(data[0].read).to.be.false | ||
61 | expect(data[1].read).to.be.false | ||
62 | expect(data[2].read).to.be.true | ||
63 | expect(data[3].read).to.be.true | ||
64 | expect(data[4].read).to.be.true | ||
65 | expect(data[5].read).to.be.false | ||
66 | }) | ||
67 | |||
68 | it('Should only list read notifications', async function () { | ||
69 | const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: false }) | ||
70 | |||
71 | for (const notification of data) { | ||
72 | expect(notification.read).to.be.true | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | it('Should only list unread notifications', async function () { | ||
77 | const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: true }) | ||
78 | |||
79 | for (const notification of data) { | ||
80 | expect(notification.read).to.be.false | ||
81 | } | ||
82 | }) | ||
83 | |||
84 | it('Should mark as read all notifications', async function () { | ||
85 | await server.notifications.markAsReadAll({ token: userToken }) | ||
86 | |||
87 | const body = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: true }) | ||
88 | |||
89 | expect(body.total).to.equal(0) | ||
90 | expect(body.data).to.have.lengthOf(0) | ||
91 | }) | ||
92 | }) | ||
93 | |||
94 | describe('Notification settings', function () { | ||
95 | let baseParams: CheckerBaseParams | ||
96 | |||
97 | before(() => { | ||
98 | baseParams = { | ||
99 | server, | ||
100 | emails, | ||
101 | socketNotifications: userNotifications, | ||
102 | token: userToken | ||
103 | } | ||
104 | }) | ||
105 | |||
106 | it('Should not have notifications', async function () { | ||
107 | this.timeout(40000) | ||
108 | |||
109 | await server.notifications.updateMySettings({ | ||
110 | token: userToken, | ||
111 | settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.NONE } | ||
112 | }) | ||
113 | |||
114 | { | ||
115 | const info = await server.users.getMyInfo({ token: userToken }) | ||
116 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.NONE) | ||
117 | } | ||
118 | |||
119 | const { name, shortUUID } = await server.videos.randomUpload() | ||
120 | |||
121 | const check = { web: true, mail: true } | ||
122 | await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' }) | ||
123 | }) | ||
124 | |||
125 | it('Should only have web notifications', async function () { | ||
126 | this.timeout(20000) | ||
127 | |||
128 | await server.notifications.updateMySettings({ | ||
129 | token: userToken, | ||
130 | settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.WEB } | ||
131 | }) | ||
132 | |||
133 | { | ||
134 | const info = await server.users.getMyInfo({ token: userToken }) | ||
135 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB) | ||
136 | } | ||
137 | |||
138 | const { name, shortUUID } = await server.videos.randomUpload() | ||
139 | |||
140 | { | ||
141 | const check = { mail: true, web: false } | ||
142 | await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' }) | ||
143 | } | ||
144 | |||
145 | { | ||
146 | const check = { mail: false, web: true } | ||
147 | await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'presence' }) | ||
148 | } | ||
149 | }) | ||
150 | |||
151 | it('Should only have mail notifications', async function () { | ||
152 | this.timeout(20000) | ||
153 | |||
154 | await server.notifications.updateMySettings({ | ||
155 | token: userToken, | ||
156 | settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.EMAIL } | ||
157 | }) | ||
158 | |||
159 | { | ||
160 | const info = await server.users.getMyInfo({ token: userToken }) | ||
161 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.EMAIL) | ||
162 | } | ||
163 | |||
164 | const { name, shortUUID } = await server.videos.randomUpload() | ||
165 | |||
166 | { | ||
167 | const check = { mail: false, web: true } | ||
168 | await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' }) | ||
169 | } | ||
170 | |||
171 | { | ||
172 | const check = { mail: true, web: false } | ||
173 | await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'presence' }) | ||
174 | } | ||
175 | }) | ||
176 | |||
177 | it('Should have email and web notifications', async function () { | ||
178 | this.timeout(20000) | ||
179 | |||
180 | await server.notifications.updateMySettings({ | ||
181 | token: userToken, | ||
182 | settings: { | ||
183 | ...getAllNotificationsSettings(), | ||
184 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
185 | } | ||
186 | }) | ||
187 | |||
188 | { | ||
189 | const info = await server.users.getMyInfo({ token: userToken }) | ||
190 | expect(info.notificationSettings.newVideoFromSubscription).to.equal( | ||
191 | UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
192 | ) | ||
193 | } | ||
194 | |||
195 | const { name, shortUUID } = await server.videos.randomUpload() | ||
196 | |||
197 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
198 | }) | ||
199 | }) | ||
200 | |||
201 | after(async function () { | ||
202 | MockSmtpServer.Instance.kill() | ||
203 | |||
204 | await cleanupTests([ server ]) | ||
205 | }) | ||
206 | }) | ||
diff --git a/packages/tests/src/api/notifications/registrations-notifications.ts b/packages/tests/src/api/notifications/registrations-notifications.ts new file mode 100644 index 000000000..1f166cb36 --- /dev/null +++ b/packages/tests/src/api/notifications/registrations-notifications.ts | |||
@@ -0,0 +1,83 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { UserNotification } from '@peertube/peertube-models' | ||
4 | import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' | ||
5 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
6 | import { CheckerBaseParams, prepareNotificationsTest, checkUserRegistered, checkRegistrationRequest } from '@tests/shared/notifications.js' | ||
7 | |||
8 | describe('Test registrations notifications', function () { | ||
9 | let server: PeerTubeServer | ||
10 | let userToken1: string | ||
11 | |||
12 | let userNotifications: UserNotification[] = [] | ||
13 | let adminNotifications: UserNotification[] = [] | ||
14 | let emails: object[] = [] | ||
15 | |||
16 | let baseParams: CheckerBaseParams | ||
17 | |||
18 | before(async function () { | ||
19 | this.timeout(120000) | ||
20 | |||
21 | const res = await prepareNotificationsTest(1) | ||
22 | |||
23 | server = res.servers[0] | ||
24 | emails = res.emails | ||
25 | userToken1 = res.userAccessToken | ||
26 | adminNotifications = res.adminNotifications | ||
27 | userNotifications = res.userNotifications | ||
28 | |||
29 | baseParams = { | ||
30 | server, | ||
31 | emails, | ||
32 | socketNotifications: adminNotifications, | ||
33 | token: server.accessToken | ||
34 | } | ||
35 | }) | ||
36 | |||
37 | describe('New direct registration for moderators', function () { | ||
38 | |||
39 | before(async function () { | ||
40 | await server.config.enableSignup(false) | ||
41 | }) | ||
42 | |||
43 | it('Should send a notification only to moderators when a user registers on the instance', async function () { | ||
44 | this.timeout(50000) | ||
45 | |||
46 | await server.registrations.register({ username: 'user_10' }) | ||
47 | |||
48 | await waitJobs([ server ]) | ||
49 | |||
50 | await checkUserRegistered({ ...baseParams, username: 'user_10', checkType: 'presence' }) | ||
51 | |||
52 | const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } } | ||
53 | await checkUserRegistered({ ...baseParams, ...userOverride, username: 'user_10', checkType: 'absence' }) | ||
54 | }) | ||
55 | }) | ||
56 | |||
57 | describe('New registration request for moderators', function () { | ||
58 | |||
59 | before(async function () { | ||
60 | await server.config.enableSignup(true) | ||
61 | }) | ||
62 | |||
63 | it('Should send a notification on new registration request', async function () { | ||
64 | this.timeout(50000) | ||
65 | |||
66 | const registrationReason = 'my reason' | ||
67 | await server.registrations.requestRegistration({ username: 'user_11', registrationReason }) | ||
68 | |||
69 | await waitJobs([ server ]) | ||
70 | |||
71 | await checkRegistrationRequest({ ...baseParams, username: 'user_11', registrationReason, checkType: 'presence' }) | ||
72 | |||
73 | const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } } | ||
74 | await checkRegistrationRequest({ ...baseParams, ...userOverride, username: 'user_11', registrationReason, checkType: 'absence' }) | ||
75 | }) | ||
76 | }) | ||
77 | |||
78 | after(async function () { | ||
79 | MockSmtpServer.Instance.kill() | ||
80 | |||
81 | await cleanupTests([ server ]) | ||
82 | }) | ||
83 | }) | ||
diff --git a/packages/tests/src/api/notifications/user-notifications.ts b/packages/tests/src/api/notifications/user-notifications.ts new file mode 100644 index 000000000..4c03cdb47 --- /dev/null +++ b/packages/tests/src/api/notifications/user-notifications.ts | |||
@@ -0,0 +1,574 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { UserNotification, UserNotificationType, VideoPrivacy, VideoStudioTask } from '@peertube/peertube-models' | ||
6 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
7 | import { cleanupTests, findExternalSavedVideo, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' | ||
8 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
9 | import { | ||
10 | prepareNotificationsTest, | ||
11 | CheckerBaseParams, | ||
12 | checkNewVideoFromSubscription, | ||
13 | checkVideoIsPublished, | ||
14 | checkVideoStudioEditionIsFinished, | ||
15 | checkMyVideoImportIsFinished, | ||
16 | checkNewActorFollow | ||
17 | } from '@tests/shared/notifications.js' | ||
18 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
19 | import { uploadRandomVideoOnServers } from '@tests/shared/videos.js' | ||
20 | |||
21 | describe('Test user notifications', function () { | ||
22 | let servers: PeerTubeServer[] = [] | ||
23 | let userAccessToken: string | ||
24 | |||
25 | let userNotifications: UserNotification[] = [] | ||
26 | let adminNotifications: UserNotification[] = [] | ||
27 | let adminNotificationsServer2: UserNotification[] = [] | ||
28 | let emails: object[] = [] | ||
29 | |||
30 | let channelId: number | ||
31 | |||
32 | before(async function () { | ||
33 | this.timeout(120000) | ||
34 | |||
35 | const res = await prepareNotificationsTest(3) | ||
36 | emails = res.emails | ||
37 | userAccessToken = res.userAccessToken | ||
38 | servers = res.servers | ||
39 | userNotifications = res.userNotifications | ||
40 | adminNotifications = res.adminNotifications | ||
41 | adminNotificationsServer2 = res.adminNotificationsServer2 | ||
42 | channelId = res.channelId | ||
43 | }) | ||
44 | |||
45 | describe('New video from my subscription notification', function () { | ||
46 | let baseParams: CheckerBaseParams | ||
47 | |||
48 | before(() => { | ||
49 | baseParams = { | ||
50 | server: servers[0], | ||
51 | emails, | ||
52 | socketNotifications: userNotifications, | ||
53 | token: userAccessToken | ||
54 | } | ||
55 | }) | ||
56 | |||
57 | it('Should not send notifications if the user does not follow the video publisher', async function () { | ||
58 | this.timeout(50000) | ||
59 | |||
60 | await uploadRandomVideoOnServers(servers, 1) | ||
61 | |||
62 | const notification = await servers[0].notifications.getLatest({ token: userAccessToken }) | ||
63 | expect(notification).to.be.undefined | ||
64 | |||
65 | expect(emails).to.have.lengthOf(0) | ||
66 | expect(userNotifications).to.have.lengthOf(0) | ||
67 | }) | ||
68 | |||
69 | it('Should send a new video notification if the user follows the local video publisher', async function () { | ||
70 | this.timeout(15000) | ||
71 | |||
72 | await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@' + servers[0].host }) | ||
73 | await waitJobs(servers) | ||
74 | |||
75 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1) | ||
76 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
77 | }) | ||
78 | |||
79 | it('Should send a new video notification from a remote account', async function () { | ||
80 | this.timeout(150000) // Server 2 has transcoding enabled | ||
81 | |||
82 | await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@' + servers[1].host }) | ||
83 | await waitJobs(servers) | ||
84 | |||
85 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2) | ||
86 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
87 | }) | ||
88 | |||
89 | it('Should send a new video notification on a scheduled publication', async function () { | ||
90 | this.timeout(50000) | ||
91 | |||
92 | // In 2 seconds | ||
93 | const updateAt = new Date(new Date().getTime() + 2000) | ||
94 | |||
95 | const data = { | ||
96 | privacy: VideoPrivacy.PRIVATE, | ||
97 | scheduleUpdate: { | ||
98 | updateAt: updateAt.toISOString(), | ||
99 | privacy: VideoPrivacy.PUBLIC | ||
100 | } | ||
101 | } | ||
102 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data) | ||
103 | |||
104 | await wait(6000) | ||
105 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
106 | }) | ||
107 | |||
108 | it('Should send a new video notification on a remote scheduled publication', async function () { | ||
109 | this.timeout(100000) | ||
110 | |||
111 | // In 2 seconds | ||
112 | const updateAt = new Date(new Date().getTime() + 2000) | ||
113 | |||
114 | const data = { | ||
115 | privacy: VideoPrivacy.PRIVATE, | ||
116 | scheduleUpdate: { | ||
117 | updateAt: updateAt.toISOString(), | ||
118 | privacy: VideoPrivacy.PUBLIC | ||
119 | } | ||
120 | } | ||
121 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data) | ||
122 | await waitJobs(servers) | ||
123 | |||
124 | await wait(6000) | ||
125 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
126 | }) | ||
127 | |||
128 | it('Should not send a notification before the video is published', async function () { | ||
129 | this.timeout(150000) | ||
130 | |||
131 | const updateAt = new Date(new Date().getTime() + 1000000) | ||
132 | |||
133 | const data = { | ||
134 | privacy: VideoPrivacy.PRIVATE, | ||
135 | scheduleUpdate: { | ||
136 | updateAt: updateAt.toISOString(), | ||
137 | privacy: VideoPrivacy.PUBLIC | ||
138 | } | ||
139 | } | ||
140 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data) | ||
141 | |||
142 | await wait(6000) | ||
143 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) | ||
144 | }) | ||
145 | |||
146 | it('Should send a new video notification when a video becomes public', async function () { | ||
147 | this.timeout(50000) | ||
148 | |||
149 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
150 | const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data) | ||
151 | |||
152 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) | ||
153 | |||
154 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
155 | |||
156 | await waitJobs(servers) | ||
157 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
158 | }) | ||
159 | |||
160 | it('Should send a new video notification when a remote video becomes public', async function () { | ||
161 | this.timeout(120000) | ||
162 | |||
163 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
164 | const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data) | ||
165 | |||
166 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) | ||
167 | |||
168 | await servers[1].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
169 | |||
170 | await waitJobs(servers) | ||
171 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
172 | }) | ||
173 | |||
174 | it('Should not send a new video notification when a video becomes unlisted', async function () { | ||
175 | this.timeout(50000) | ||
176 | |||
177 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
178 | const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data) | ||
179 | |||
180 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } }) | ||
181 | |||
182 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) | ||
183 | }) | ||
184 | |||
185 | it('Should not send a new video notification when a remote video becomes unlisted', async function () { | ||
186 | this.timeout(100000) | ||
187 | |||
188 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
189 | const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data) | ||
190 | |||
191 | await servers[1].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } }) | ||
192 | |||
193 | await waitJobs(servers) | ||
194 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) | ||
195 | }) | ||
196 | |||
197 | it('Should send a new video notification after a video import', async function () { | ||
198 | this.timeout(100000) | ||
199 | |||
200 | const name = 'video import ' + buildUUID() | ||
201 | |||
202 | const attributes = { | ||
203 | name, | ||
204 | channelId, | ||
205 | privacy: VideoPrivacy.PUBLIC, | ||
206 | targetUrl: FIXTURE_URLS.goodVideo | ||
207 | } | ||
208 | const { video } = await servers[0].imports.importVideo({ attributes }) | ||
209 | |||
210 | await waitJobs(servers) | ||
211 | |||
212 | await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' }) | ||
213 | }) | ||
214 | }) | ||
215 | |||
216 | describe('My video is published', function () { | ||
217 | let baseParams: CheckerBaseParams | ||
218 | |||
219 | before(() => { | ||
220 | baseParams = { | ||
221 | server: servers[1], | ||
222 | emails, | ||
223 | socketNotifications: adminNotificationsServer2, | ||
224 | token: servers[1].accessToken | ||
225 | } | ||
226 | }) | ||
227 | |||
228 | it('Should not send a notification if transcoding is not enabled', async function () { | ||
229 | this.timeout(50000) | ||
230 | |||
231 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1) | ||
232 | await waitJobs(servers) | ||
233 | |||
234 | await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) | ||
235 | }) | ||
236 | |||
237 | it('Should not send a notification if the wait transcoding is false', async function () { | ||
238 | this.timeout(100_000) | ||
239 | |||
240 | await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: false }) | ||
241 | await waitJobs(servers) | ||
242 | |||
243 | const notification = await servers[0].notifications.getLatest({ token: userAccessToken }) | ||
244 | if (notification) { | ||
245 | expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED) | ||
246 | } | ||
247 | }) | ||
248 | |||
249 | it('Should send a notification even if the video is not transcoded in other resolutions', async function () { | ||
250 | this.timeout(100_000) | ||
251 | |||
252 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' }) | ||
253 | await waitJobs(servers) | ||
254 | |||
255 | await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
256 | }) | ||
257 | |||
258 | it('Should send a notification with a transcoded video', async function () { | ||
259 | this.timeout(100_000) | ||
260 | |||
261 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true }) | ||
262 | await waitJobs(servers) | ||
263 | |||
264 | await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
265 | }) | ||
266 | |||
267 | it('Should send a notification when an imported video is transcoded', async function () { | ||
268 | this.timeout(120000) | ||
269 | |||
270 | const name = 'video import ' + buildUUID() | ||
271 | |||
272 | const attributes = { | ||
273 | name, | ||
274 | channelId, | ||
275 | privacy: VideoPrivacy.PUBLIC, | ||
276 | targetUrl: FIXTURE_URLS.goodVideo, | ||
277 | waitTranscoding: true | ||
278 | } | ||
279 | const { video } = await servers[1].imports.importVideo({ attributes }) | ||
280 | |||
281 | await waitJobs(servers) | ||
282 | await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' }) | ||
283 | }) | ||
284 | |||
285 | it('Should send a notification when the scheduled update has been proceeded', async function () { | ||
286 | this.timeout(70000) | ||
287 | |||
288 | // In 2 seconds | ||
289 | const updateAt = new Date(new Date().getTime() + 2000) | ||
290 | |||
291 | const data = { | ||
292 | privacy: VideoPrivacy.PRIVATE, | ||
293 | scheduleUpdate: { | ||
294 | updateAt: updateAt.toISOString(), | ||
295 | privacy: VideoPrivacy.PUBLIC | ||
296 | } | ||
297 | } | ||
298 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data) | ||
299 | |||
300 | await wait(6000) | ||
301 | await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
302 | }) | ||
303 | |||
304 | it('Should not send a notification before the video is published', async function () { | ||
305 | this.timeout(150000) | ||
306 | |||
307 | const updateAt = new Date(new Date().getTime() + 1000000) | ||
308 | |||
309 | const data = { | ||
310 | privacy: VideoPrivacy.PRIVATE, | ||
311 | scheduleUpdate: { | ||
312 | updateAt: updateAt.toISOString(), | ||
313 | privacy: VideoPrivacy.PUBLIC | ||
314 | } | ||
315 | } | ||
316 | const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data) | ||
317 | |||
318 | await wait(6000) | ||
319 | await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' }) | ||
320 | }) | ||
321 | }) | ||
322 | |||
323 | describe('My live replay is published', function () { | ||
324 | |||
325 | let baseParams: CheckerBaseParams | ||
326 | |||
327 | before(() => { | ||
328 | baseParams = { | ||
329 | server: servers[1], | ||
330 | emails, | ||
331 | socketNotifications: adminNotificationsServer2, | ||
332 | token: servers[1].accessToken | ||
333 | } | ||
334 | }) | ||
335 | |||
336 | it('Should send a notification is a live replay of a non permanent live is published', async function () { | ||
337 | this.timeout(120000) | ||
338 | |||
339 | const { shortUUID } = await servers[1].live.create({ | ||
340 | fields: { | ||
341 | name: 'non permanent live', | ||
342 | privacy: VideoPrivacy.PUBLIC, | ||
343 | channelId: servers[1].store.channel.id, | ||
344 | saveReplay: true, | ||
345 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
346 | permanentLive: false | ||
347 | } | ||
348 | }) | ||
349 | |||
350 | const ffmpegCommand = await servers[1].live.sendRTMPStreamInVideo({ videoId: shortUUID }) | ||
351 | |||
352 | await waitJobs(servers) | ||
353 | await servers[1].live.waitUntilPublished({ videoId: shortUUID }) | ||
354 | |||
355 | await stopFfmpeg(ffmpegCommand) | ||
356 | await servers[1].live.waitUntilReplacedByReplay({ videoId: shortUUID }) | ||
357 | |||
358 | await waitJobs(servers) | ||
359 | await checkVideoIsPublished({ ...baseParams, videoName: 'non permanent live', shortUUID, checkType: 'presence' }) | ||
360 | }) | ||
361 | |||
362 | it('Should send a notification is a live replay of a permanent live is published', async function () { | ||
363 | this.timeout(120000) | ||
364 | |||
365 | const { shortUUID } = await servers[1].live.create({ | ||
366 | fields: { | ||
367 | name: 'permanent live', | ||
368 | privacy: VideoPrivacy.PUBLIC, | ||
369 | channelId: servers[1].store.channel.id, | ||
370 | saveReplay: true, | ||
371 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
372 | permanentLive: true | ||
373 | } | ||
374 | }) | ||
375 | |||
376 | const ffmpegCommand = await servers[1].live.sendRTMPStreamInVideo({ videoId: shortUUID }) | ||
377 | |||
378 | await waitJobs(servers) | ||
379 | await servers[1].live.waitUntilPublished({ videoId: shortUUID }) | ||
380 | |||
381 | const liveDetails = await servers[1].videos.get({ id: shortUUID }) | ||
382 | |||
383 | await stopFfmpeg(ffmpegCommand) | ||
384 | |||
385 | await servers[1].live.waitUntilWaiting({ videoId: shortUUID }) | ||
386 | await waitJobs(servers) | ||
387 | |||
388 | const video = await findExternalSavedVideo(servers[1], liveDetails) | ||
389 | expect(video).to.exist | ||
390 | |||
391 | await checkVideoIsPublished({ ...baseParams, videoName: video.name, shortUUID: video.shortUUID, checkType: 'presence' }) | ||
392 | }) | ||
393 | }) | ||
394 | |||
395 | describe('Video studio', function () { | ||
396 | let baseParams: CheckerBaseParams | ||
397 | |||
398 | before(() => { | ||
399 | baseParams = { | ||
400 | server: servers[1], | ||
401 | emails, | ||
402 | socketNotifications: adminNotificationsServer2, | ||
403 | token: servers[1].accessToken | ||
404 | } | ||
405 | }) | ||
406 | |||
407 | it('Should send a notification after studio edition', async function () { | ||
408 | this.timeout(240000) | ||
409 | |||
410 | const { name, shortUUID, id } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true }) | ||
411 | |||
412 | await waitJobs(servers) | ||
413 | await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
414 | |||
415 | const tasks: VideoStudioTask[] = [ | ||
416 | { | ||
417 | name: 'cut', | ||
418 | options: { | ||
419 | start: 0, | ||
420 | end: 1 | ||
421 | } | ||
422 | } | ||
423 | ] | ||
424 | await servers[1].videoStudio.createEditionTasks({ videoId: id, tasks }) | ||
425 | await waitJobs(servers) | ||
426 | |||
427 | await checkVideoStudioEditionIsFinished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' }) | ||
428 | }) | ||
429 | }) | ||
430 | |||
431 | describe('My video is imported', function () { | ||
432 | let baseParams: CheckerBaseParams | ||
433 | |||
434 | before(() => { | ||
435 | baseParams = { | ||
436 | server: servers[0], | ||
437 | emails, | ||
438 | socketNotifications: adminNotifications, | ||
439 | token: servers[0].accessToken | ||
440 | } | ||
441 | }) | ||
442 | |||
443 | it('Should send a notification when the video import failed', async function () { | ||
444 | this.timeout(70000) | ||
445 | |||
446 | const name = 'video import ' + buildUUID() | ||
447 | |||
448 | const attributes = { | ||
449 | name, | ||
450 | channelId, | ||
451 | privacy: VideoPrivacy.PRIVATE, | ||
452 | targetUrl: FIXTURE_URLS.badVideo | ||
453 | } | ||
454 | const { video: { shortUUID } } = await servers[0].imports.importVideo({ attributes }) | ||
455 | |||
456 | await waitJobs(servers) | ||
457 | |||
458 | const url = FIXTURE_URLS.badVideo | ||
459 | await checkMyVideoImportIsFinished({ ...baseParams, videoName: name, shortUUID, url, success: false, checkType: 'presence' }) | ||
460 | }) | ||
461 | |||
462 | it('Should send a notification when the video import succeeded', async function () { | ||
463 | this.timeout(70000) | ||
464 | |||
465 | const name = 'video import ' + buildUUID() | ||
466 | |||
467 | const attributes = { | ||
468 | name, | ||
469 | channelId, | ||
470 | privacy: VideoPrivacy.PRIVATE, | ||
471 | targetUrl: FIXTURE_URLS.goodVideo | ||
472 | } | ||
473 | const { video: { shortUUID } } = await servers[0].imports.importVideo({ attributes }) | ||
474 | |||
475 | await waitJobs(servers) | ||
476 | |||
477 | const url = FIXTURE_URLS.goodVideo | ||
478 | await checkMyVideoImportIsFinished({ ...baseParams, videoName: name, shortUUID, url, success: true, checkType: 'presence' }) | ||
479 | }) | ||
480 | }) | ||
481 | |||
482 | describe('New actor follow', function () { | ||
483 | let baseParams: CheckerBaseParams | ||
484 | const myChannelName = 'super channel name' | ||
485 | const myUserName = 'super user name' | ||
486 | |||
487 | before(async function () { | ||
488 | baseParams = { | ||
489 | server: servers[0], | ||
490 | emails, | ||
491 | socketNotifications: userNotifications, | ||
492 | token: userAccessToken | ||
493 | } | ||
494 | |||
495 | await servers[0].users.updateMe({ displayName: 'super root name' }) | ||
496 | |||
497 | await servers[0].users.updateMe({ | ||
498 | token: userAccessToken, | ||
499 | displayName: myUserName | ||
500 | }) | ||
501 | |||
502 | await servers[1].users.updateMe({ displayName: 'super root 2 name' }) | ||
503 | |||
504 | await servers[0].channels.update({ | ||
505 | token: userAccessToken, | ||
506 | channelName: 'user_1_channel', | ||
507 | attributes: { displayName: myChannelName } | ||
508 | }) | ||
509 | }) | ||
510 | |||
511 | it('Should notify when a local channel is following one of our channel', async function () { | ||
512 | this.timeout(50000) | ||
513 | |||
514 | await servers[0].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host }) | ||
515 | await waitJobs(servers) | ||
516 | |||
517 | await checkNewActorFollow({ | ||
518 | ...baseParams, | ||
519 | followType: 'channel', | ||
520 | followerName: 'root', | ||
521 | followerDisplayName: 'super root name', | ||
522 | followingDisplayName: myChannelName, | ||
523 | checkType: 'presence' | ||
524 | }) | ||
525 | |||
526 | await servers[0].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host }) | ||
527 | }) | ||
528 | |||
529 | it('Should notify when a remote channel is following one of our channel', async function () { | ||
530 | this.timeout(50000) | ||
531 | |||
532 | await servers[1].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host }) | ||
533 | await waitJobs(servers) | ||
534 | |||
535 | await checkNewActorFollow({ | ||
536 | ...baseParams, | ||
537 | followType: 'channel', | ||
538 | followerName: 'root', | ||
539 | followerDisplayName: 'super root 2 name', | ||
540 | followingDisplayName: myChannelName, | ||
541 | checkType: 'presence' | ||
542 | }) | ||
543 | |||
544 | await servers[1].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host }) | ||
545 | }) | ||
546 | |||
547 | // PeerTube does not support account -> account follows | ||
548 | // it('Should notify when a local account is following one of our channel', async function () { | ||
549 | // this.timeout(50000) | ||
550 | // | ||
551 | // await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@' + servers[0].host) | ||
552 | // | ||
553 | // await waitJobs(servers) | ||
554 | // | ||
555 | // await checkNewActorFollow(baseParams, 'account', 'root', 'super root name', myUserName, 'presence') | ||
556 | // }) | ||
557 | |||
558 | // it('Should notify when a remote account is following one of our channel', async function () { | ||
559 | // this.timeout(50000) | ||
560 | // | ||
561 | // await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@' + servers[0].host) | ||
562 | // | ||
563 | // await waitJobs(servers) | ||
564 | // | ||
565 | // await checkNewActorFollow(baseParams, 'account', 'root', 'super root 2 name', myUserName, 'presence') | ||
566 | // }) | ||
567 | }) | ||
568 | |||
569 | after(async function () { | ||
570 | MockSmtpServer.Instance.kill() | ||
571 | |||
572 | await cleanupTests(servers) | ||
573 | }) | ||
574 | }) | ||
diff --git a/packages/tests/src/api/object-storage/index.ts b/packages/tests/src/api/object-storage/index.ts new file mode 100644 index 000000000..51d2a29a0 --- /dev/null +++ b/packages/tests/src/api/object-storage/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './live.js' | ||
2 | export * from './video-imports.js' | ||
3 | export * from './video-static-file-privacy.js' | ||
4 | export * from './videos.js' | ||
diff --git a/packages/tests/src/api/object-storage/live.ts b/packages/tests/src/api/object-storage/live.ts new file mode 100644 index 000000000..c8c214af5 --- /dev/null +++ b/packages/tests/src/api/object-storage/live.ts | |||
@@ -0,0 +1,314 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | findExternalSavedVideo, | ||
11 | makeRawRequest, | ||
12 | ObjectStorageCommand, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | stopFfmpeg, | ||
17 | waitJobs, | ||
18 | waitUntilLivePublishedOnAllServers, | ||
19 | waitUntilLiveReplacedByReplayOnAllServers, | ||
20 | waitUntilLiveWaitingOnAllServers | ||
21 | } from '@peertube/peertube-server-commands' | ||
22 | import { expectStartWith } from '@tests/shared/checks.js' | ||
23 | import { testLiveVideoResolutions } from '@tests/shared/live.js' | ||
24 | import { MockObjectStorageProxy } from '@tests/shared/mock-servers/mock-object-storage.js' | ||
25 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
26 | |||
27 | async function createLive (server: PeerTubeServer, permanent: boolean) { | ||
28 | const attributes: LiveVideoCreate = { | ||
29 | channelId: server.store.channel.id, | ||
30 | privacy: VideoPrivacy.PUBLIC, | ||
31 | name: 'my super live', | ||
32 | saveReplay: true, | ||
33 | replaySettings: { privacy: VideoPrivacy.PUBLIC }, | ||
34 | permanentLive: permanent | ||
35 | } | ||
36 | |||
37 | const { uuid } = await server.live.create({ fields: attributes }) | ||
38 | |||
39 | return uuid | ||
40 | } | ||
41 | |||
42 | async function checkFilesExist (options: { | ||
43 | servers: PeerTubeServer[] | ||
44 | videoUUID: string | ||
45 | numberOfFiles: number | ||
46 | objectStorage: ObjectStorageCommand | ||
47 | }) { | ||
48 | const { servers, videoUUID, numberOfFiles, objectStorage } = options | ||
49 | |||
50 | for (const server of servers) { | ||
51 | const video = await server.videos.get({ id: videoUUID }) | ||
52 | |||
53 | expect(video.files).to.have.lengthOf(0) | ||
54 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
55 | |||
56 | const files = video.streamingPlaylists[0].files | ||
57 | expect(files).to.have.lengthOf(numberOfFiles) | ||
58 | |||
59 | for (const file of files) { | ||
60 | expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
61 | |||
62 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
63 | } | ||
64 | } | ||
65 | } | ||
66 | |||
67 | async function checkFilesCleanup (options: { | ||
68 | server: PeerTubeServer | ||
69 | videoUUID: string | ||
70 | resolutions: number[] | ||
71 | objectStorage: ObjectStorageCommand | ||
72 | }) { | ||
73 | const { server, videoUUID, resolutions, objectStorage } = options | ||
74 | |||
75 | const resolutionFiles = resolutions.map((_value, i) => `${i}.m3u8`) | ||
76 | |||
77 | for (const playlistName of [ 'master.m3u8' ].concat(resolutionFiles)) { | ||
78 | await server.live.getPlaylistFile({ | ||
79 | videoUUID, | ||
80 | playlistName, | ||
81 | expectedStatus: HttpStatusCode.NOT_FOUND_404, | ||
82 | objectStorage | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | await server.live.getSegmentFile({ | ||
87 | videoUUID, | ||
88 | playlistNumber: 0, | ||
89 | segment: 0, | ||
90 | objectStorage, | ||
91 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
92 | }) | ||
93 | } | ||
94 | |||
95 | describe('Object storage for lives', function () { | ||
96 | if (areMockObjectStorageTestsDisabled()) return | ||
97 | |||
98 | let servers: PeerTubeServer[] | ||
99 | let sqlCommandServer1: SQLCommand | ||
100 | const objectStorage = new ObjectStorageCommand() | ||
101 | |||
102 | before(async function () { | ||
103 | this.timeout(120000) | ||
104 | |||
105 | await objectStorage.prepareDefaultMockBuckets() | ||
106 | servers = await createMultipleServers(2, objectStorage.getDefaultMockConfig()) | ||
107 | |||
108 | await setAccessTokensToServers(servers) | ||
109 | await setDefaultVideoChannel(servers) | ||
110 | await doubleFollow(servers[0], servers[1]) | ||
111 | |||
112 | await servers[0].config.enableTranscoding() | ||
113 | |||
114 | sqlCommandServer1 = new SQLCommand(servers[0]) | ||
115 | }) | ||
116 | |||
117 | describe('Without live transcoding', function () { | ||
118 | let videoUUID: string | ||
119 | |||
120 | before(async function () { | ||
121 | await servers[0].config.enableLive({ transcoding: false }) | ||
122 | |||
123 | videoUUID = await createLive(servers[0], false) | ||
124 | }) | ||
125 | |||
126 | it('Should create a live and publish it on object storage', async function () { | ||
127 | this.timeout(220000) | ||
128 | |||
129 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID }) | ||
130 | await waitUntilLivePublishedOnAllServers(servers, videoUUID) | ||
131 | |||
132 | await testLiveVideoResolutions({ | ||
133 | originServer: servers[0], | ||
134 | sqlCommand: sqlCommandServer1, | ||
135 | servers, | ||
136 | liveVideoId: videoUUID, | ||
137 | resolutions: [ 720 ], | ||
138 | transcoded: false, | ||
139 | objectStorage | ||
140 | }) | ||
141 | |||
142 | await stopFfmpeg(ffmpegCommand) | ||
143 | }) | ||
144 | |||
145 | it('Should have saved the replay on object storage', async function () { | ||
146 | this.timeout(220000) | ||
147 | |||
148 | await waitUntilLiveReplacedByReplayOnAllServers(servers, videoUUID) | ||
149 | await waitJobs(servers) | ||
150 | |||
151 | await checkFilesExist({ servers, videoUUID, numberOfFiles: 1, objectStorage }) | ||
152 | }) | ||
153 | |||
154 | it('Should have cleaned up live files from object storage', async function () { | ||
155 | await checkFilesCleanup({ server: servers[0], videoUUID, resolutions: [ 720 ], objectStorage }) | ||
156 | }) | ||
157 | }) | ||
158 | |||
159 | describe('With live transcoding', function () { | ||
160 | const resolutions = [ 720, 480, 360, 240, 144 ] | ||
161 | |||
162 | before(async function () { | ||
163 | await servers[0].config.enableLive({ transcoding: true }) | ||
164 | }) | ||
165 | |||
166 | describe('Normal replay', function () { | ||
167 | let videoUUIDNonPermanent: string | ||
168 | |||
169 | before(async function () { | ||
170 | videoUUIDNonPermanent = await createLive(servers[0], false) | ||
171 | }) | ||
172 | |||
173 | it('Should create a live and publish it on object storage', async function () { | ||
174 | this.timeout(240000) | ||
175 | |||
176 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDNonPermanent }) | ||
177 | await waitUntilLivePublishedOnAllServers(servers, videoUUIDNonPermanent) | ||
178 | |||
179 | await testLiveVideoResolutions({ | ||
180 | originServer: servers[0], | ||
181 | sqlCommand: sqlCommandServer1, | ||
182 | servers, | ||
183 | liveVideoId: videoUUIDNonPermanent, | ||
184 | resolutions, | ||
185 | transcoded: true, | ||
186 | objectStorage | ||
187 | }) | ||
188 | |||
189 | await stopFfmpeg(ffmpegCommand) | ||
190 | }) | ||
191 | |||
192 | it('Should have saved the replay on object storage', async function () { | ||
193 | this.timeout(220000) | ||
194 | |||
195 | await waitUntilLiveReplacedByReplayOnAllServers(servers, videoUUIDNonPermanent) | ||
196 | await waitJobs(servers) | ||
197 | |||
198 | await checkFilesExist({ servers, videoUUID: videoUUIDNonPermanent, numberOfFiles: 5, objectStorage }) | ||
199 | }) | ||
200 | |||
201 | it('Should have cleaned up live files from object storage', async function () { | ||
202 | await checkFilesCleanup({ server: servers[0], videoUUID: videoUUIDNonPermanent, resolutions, objectStorage }) | ||
203 | }) | ||
204 | }) | ||
205 | |||
206 | describe('Permanent replay', function () { | ||
207 | let videoUUIDPermanent: string | ||
208 | |||
209 | before(async function () { | ||
210 | videoUUIDPermanent = await createLive(servers[0], true) | ||
211 | }) | ||
212 | |||
213 | it('Should create a live and publish it on object storage', async function () { | ||
214 | this.timeout(240000) | ||
215 | |||
216 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDPermanent }) | ||
217 | await waitUntilLivePublishedOnAllServers(servers, videoUUIDPermanent) | ||
218 | |||
219 | await testLiveVideoResolutions({ | ||
220 | originServer: servers[0], | ||
221 | sqlCommand: sqlCommandServer1, | ||
222 | servers, | ||
223 | liveVideoId: videoUUIDPermanent, | ||
224 | resolutions, | ||
225 | transcoded: true, | ||
226 | objectStorage | ||
227 | }) | ||
228 | |||
229 | await stopFfmpeg(ffmpegCommand) | ||
230 | }) | ||
231 | |||
232 | it('Should have saved the replay on object storage', async function () { | ||
233 | this.timeout(220000) | ||
234 | |||
235 | await waitUntilLiveWaitingOnAllServers(servers, videoUUIDPermanent) | ||
236 | await waitJobs(servers) | ||
237 | |||
238 | const videoLiveDetails = await servers[0].videos.get({ id: videoUUIDPermanent }) | ||
239 | const replay = await findExternalSavedVideo(servers[0], videoLiveDetails) | ||
240 | |||
241 | await checkFilesExist({ servers, videoUUID: replay.uuid, numberOfFiles: 5, objectStorage }) | ||
242 | }) | ||
243 | |||
244 | it('Should have cleaned up live files from object storage', async function () { | ||
245 | await checkFilesCleanup({ server: servers[0], videoUUID: videoUUIDPermanent, resolutions, objectStorage }) | ||
246 | }) | ||
247 | }) | ||
248 | }) | ||
249 | |||
250 | describe('With object storage base url', function () { | ||
251 | const mockObjectStorageProxy = new MockObjectStorageProxy() | ||
252 | let baseMockUrl: string | ||
253 | |||
254 | before(async function () { | ||
255 | this.timeout(120000) | ||
256 | |||
257 | const port = await mockObjectStorageProxy.initialize() | ||
258 | const bucketName = objectStorage.getMockStreamingPlaylistsBucketName() | ||
259 | baseMockUrl = `http://127.0.0.1:${port}/${bucketName}` | ||
260 | |||
261 | await objectStorage.prepareDefaultMockBuckets() | ||
262 | |||
263 | const config = { | ||
264 | object_storage: { | ||
265 | enabled: true, | ||
266 | endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(), | ||
267 | region: ObjectStorageCommand.getMockRegion(), | ||
268 | |||
269 | credentials: ObjectStorageCommand.getMockCredentialsConfig(), | ||
270 | |||
271 | streaming_playlists: { | ||
272 | bucket_name: bucketName, | ||
273 | prefix: '', | ||
274 | base_url: baseMockUrl | ||
275 | } | ||
276 | } | ||
277 | } | ||
278 | |||
279 | await servers[0].kill() | ||
280 | await servers[0].run(config) | ||
281 | |||
282 | await servers[0].config.enableLive({ transcoding: true, resolutions: 'min' }) | ||
283 | }) | ||
284 | |||
285 | it('Should publish a live and replace the base url', async function () { | ||
286 | this.timeout(240000) | ||
287 | |||
288 | const videoUUIDPermanent = await createLive(servers[0], true) | ||
289 | |||
290 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDPermanent }) | ||
291 | await waitUntilLivePublishedOnAllServers(servers, videoUUIDPermanent) | ||
292 | |||
293 | await testLiveVideoResolutions({ | ||
294 | originServer: servers[0], | ||
295 | sqlCommand: sqlCommandServer1, | ||
296 | servers, | ||
297 | liveVideoId: videoUUIDPermanent, | ||
298 | resolutions: [ 720 ], | ||
299 | transcoded: true, | ||
300 | objectStorage, | ||
301 | objectStorageBaseUrl: baseMockUrl | ||
302 | }) | ||
303 | |||
304 | await stopFfmpeg(ffmpegCommand) | ||
305 | }) | ||
306 | }) | ||
307 | |||
308 | after(async function () { | ||
309 | await sqlCommandServer1.cleanup() | ||
310 | await objectStorage.cleanupMock() | ||
311 | |||
312 | await cleanupTests(servers) | ||
313 | }) | ||
314 | }) | ||
diff --git a/packages/tests/src/api/object-storage/video-imports.ts b/packages/tests/src/api/object-storage/video-imports.ts new file mode 100644 index 000000000..43f769842 --- /dev/null +++ b/packages/tests/src/api/object-storage/video-imports.ts | |||
@@ -0,0 +1,112 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { expectStartWith } from '@tests/shared/checks.js' | ||
5 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
6 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
7 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | makeRawRequest, | ||
12 | ObjectStorageCommand, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | waitJobs | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | |||
19 | async function importVideo (server: PeerTubeServer) { | ||
20 | const attributes = { | ||
21 | name: 'import 2', | ||
22 | privacy: VideoPrivacy.PUBLIC, | ||
23 | channelId: server.store.channel.id, | ||
24 | targetUrl: FIXTURE_URLS.goodVideo720 | ||
25 | } | ||
26 | |||
27 | const { video: { uuid } } = await server.imports.importVideo({ attributes }) | ||
28 | |||
29 | return uuid | ||
30 | } | ||
31 | |||
32 | describe('Object storage for video import', function () { | ||
33 | if (areMockObjectStorageTestsDisabled()) return | ||
34 | |||
35 | let server: PeerTubeServer | ||
36 | const objectStorage = new ObjectStorageCommand() | ||
37 | |||
38 | before(async function () { | ||
39 | this.timeout(120000) | ||
40 | |||
41 | await objectStorage.prepareDefaultMockBuckets() | ||
42 | |||
43 | server = await createSingleServer(1, objectStorage.getDefaultMockConfig()) | ||
44 | |||
45 | await setAccessTokensToServers([ server ]) | ||
46 | await setDefaultVideoChannel([ server ]) | ||
47 | |||
48 | await server.config.enableImports() | ||
49 | }) | ||
50 | |||
51 | describe('Without transcoding', async function () { | ||
52 | |||
53 | before(async function () { | ||
54 | await server.config.disableTranscoding() | ||
55 | }) | ||
56 | |||
57 | it('Should import a video and have sent it to object storage', async function () { | ||
58 | this.timeout(120000) | ||
59 | |||
60 | const uuid = await importVideo(server) | ||
61 | await waitJobs(server) | ||
62 | |||
63 | const video = await server.videos.get({ id: uuid }) | ||
64 | |||
65 | expect(video.files).to.have.lengthOf(1) | ||
66 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
67 | |||
68 | const fileUrl = video.files[0].fileUrl | ||
69 | expectStartWith(fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
70 | |||
71 | await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
72 | }) | ||
73 | }) | ||
74 | |||
75 | describe('With transcoding', async function () { | ||
76 | |||
77 | before(async function () { | ||
78 | await server.config.enableTranscoding() | ||
79 | }) | ||
80 | |||
81 | it('Should import a video and have sent it to object storage', async function () { | ||
82 | this.timeout(120000) | ||
83 | |||
84 | const uuid = await importVideo(server) | ||
85 | await waitJobs(server) | ||
86 | |||
87 | const video = await server.videos.get({ id: uuid }) | ||
88 | |||
89 | expect(video.files).to.have.lengthOf(5) | ||
90 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
91 | expect(video.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
92 | |||
93 | for (const file of video.files) { | ||
94 | expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
95 | |||
96 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
97 | } | ||
98 | |||
99 | for (const file of video.streamingPlaylists[0].files) { | ||
100 | expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
101 | |||
102 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
103 | } | ||
104 | }) | ||
105 | }) | ||
106 | |||
107 | after(async function () { | ||
108 | await objectStorage.cleanupMock() | ||
109 | |||
110 | await cleanupTests([ server ]) | ||
111 | }) | ||
112 | }) | ||
diff --git a/packages/tests/src/api/object-storage/video-static-file-privacy.ts b/packages/tests/src/api/object-storage/video-static-file-privacy.ts new file mode 100644 index 000000000..cf6e9b4b9 --- /dev/null +++ b/packages/tests/src/api/object-storage/video-static-file-privacy.ts | |||
@@ -0,0 +1,573 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename } from 'path' | ||
5 | import { getAllFiles, getHLS } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { areScalewayObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | findExternalSavedVideo, | ||
12 | makeRawRequest, | ||
13 | ObjectStorageCommand, | ||
14 | PeerTubeServer, | ||
15 | sendRTMPStream, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel, | ||
18 | stopFfmpeg, | ||
19 | waitJobs | ||
20 | } from '@peertube/peertube-server-commands' | ||
21 | import { expectStartWith } from '@tests/shared/checks.js' | ||
22 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
23 | import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js' | ||
24 | |||
25 | function extractFilenameFromUrl (url: string) { | ||
26 | const parts = basename(url).split(':') | ||
27 | |||
28 | return parts[parts.length - 1] | ||
29 | } | ||
30 | |||
31 | describe('Object storage for video static file privacy', function () { | ||
32 | // We need real world object storage to check ACL | ||
33 | if (areScalewayObjectStorageTestsDisabled()) return | ||
34 | |||
35 | let server: PeerTubeServer | ||
36 | let sqlCommand: SQLCommand | ||
37 | let userToken: string | ||
38 | |||
39 | // --------------------------------------------------------------------------- | ||
40 | |||
41 | async function checkPrivateVODFiles (uuid: string) { | ||
42 | const video = await server.videos.getWithToken({ id: uuid }) | ||
43 | |||
44 | for (const file of video.files) { | ||
45 | expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/web-videos/private/') | ||
46 | |||
47 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
48 | } | ||
49 | |||
50 | for (const file of getAllFiles(video)) { | ||
51 | const internalFileUrl = await sqlCommand.getInternalFileUrl(file.id) | ||
52 | expectStartWith(internalFileUrl, ObjectStorageCommand.getScalewayBaseUrl()) | ||
53 | await makeRawRequest({ url: internalFileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
54 | } | ||
55 | |||
56 | const hls = getHLS(video) | ||
57 | |||
58 | if (hls) { | ||
59 | for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { | ||
60 | expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/') | ||
61 | } | ||
62 | |||
63 | await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
64 | await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
65 | |||
66 | for (const file of hls.files) { | ||
67 | expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/streaming-playlists/hls/private/') | ||
68 | |||
69 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
70 | } | ||
71 | } | ||
72 | } | ||
73 | |||
74 | async function checkPublicVODFiles (uuid: string) { | ||
75 | const video = await server.videos.getWithToken({ id: uuid }) | ||
76 | |||
77 | for (const file of getAllFiles(video)) { | ||
78 | expectStartWith(file.fileUrl, ObjectStorageCommand.getScalewayBaseUrl()) | ||
79 | |||
80 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
81 | } | ||
82 | |||
83 | const hls = getHLS(video) | ||
84 | |||
85 | if (hls) { | ||
86 | expectStartWith(hls.playlistUrl, ObjectStorageCommand.getScalewayBaseUrl()) | ||
87 | expectStartWith(hls.segmentsSha256Url, ObjectStorageCommand.getScalewayBaseUrl()) | ||
88 | |||
89 | await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
90 | await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
91 | } | ||
92 | } | ||
93 | |||
94 | // --------------------------------------------------------------------------- | ||
95 | |||
96 | before(async function () { | ||
97 | this.timeout(120000) | ||
98 | |||
99 | server = await createSingleServer(1, ObjectStorageCommand.getDefaultScalewayConfig({ serverNumber: 1 })) | ||
100 | await setAccessTokensToServers([ server ]) | ||
101 | await setDefaultVideoChannel([ server ]) | ||
102 | |||
103 | await server.config.enableMinimumTranscoding() | ||
104 | |||
105 | userToken = await server.users.generateUserAndToken('user1') | ||
106 | |||
107 | sqlCommand = new SQLCommand(server) | ||
108 | }) | ||
109 | |||
110 | describe('VOD', function () { | ||
111 | let privateVideoUUID: string | ||
112 | let publicVideoUUID: string | ||
113 | let passwordProtectedVideoUUID: string | ||
114 | let userPrivateVideoUUID: string | ||
115 | |||
116 | const correctPassword = 'my super password' | ||
117 | const correctPasswordHeader = { 'x-peertube-video-password': correctPassword } | ||
118 | const incorrectPasswordHeader = { 'x-peertube-video-password': correctPassword + 'toto' } | ||
119 | |||
120 | // --------------------------------------------------------------------------- | ||
121 | |||
122 | async function getSampleFileUrls (videoId: string) { | ||
123 | const video = await server.videos.getWithToken({ id: videoId }) | ||
124 | |||
125 | return { | ||
126 | webVideoFile: video.files[0].fileUrl, | ||
127 | hlsFile: getHLS(video).files[0].fileUrl | ||
128 | } | ||
129 | } | ||
130 | |||
131 | // --------------------------------------------------------------------------- | ||
132 | |||
133 | it('Should upload a private video and have appropriate object storage ACL', async function () { | ||
134 | this.timeout(120000) | ||
135 | |||
136 | { | ||
137 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
138 | privateVideoUUID = uuid | ||
139 | } | ||
140 | |||
141 | { | ||
142 | const { uuid } = await server.videos.quickUpload({ name: 'user video', token: userToken, privacy: VideoPrivacy.PRIVATE }) | ||
143 | userPrivateVideoUUID = uuid | ||
144 | } | ||
145 | |||
146 | await waitJobs([ server ]) | ||
147 | |||
148 | await checkPrivateVODFiles(privateVideoUUID) | ||
149 | }) | ||
150 | |||
151 | it('Should upload a password protected video and have appropriate object storage ACL', async function () { | ||
152 | this.timeout(120000) | ||
153 | |||
154 | { | ||
155 | const { uuid } = await server.videos.quickUpload({ | ||
156 | name: 'video', | ||
157 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
158 | videoPasswords: [ correctPassword ] | ||
159 | }) | ||
160 | passwordProtectedVideoUUID = uuid | ||
161 | } | ||
162 | await waitJobs([ server ]) | ||
163 | |||
164 | await checkPrivateVODFiles(passwordProtectedVideoUUID) | ||
165 | }) | ||
166 | |||
167 | it('Should upload a public video and have appropriate object storage ACL', async function () { | ||
168 | this.timeout(120000) | ||
169 | |||
170 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.UNLISTED }) | ||
171 | await waitJobs([ server ]) | ||
172 | |||
173 | publicVideoUUID = uuid | ||
174 | |||
175 | await checkPublicVODFiles(publicVideoUUID) | ||
176 | }) | ||
177 | |||
178 | it('Should not get files without appropriate OAuth token', async function () { | ||
179 | this.timeout(60000) | ||
180 | |||
181 | const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) | ||
182 | |||
183 | await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
184 | await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
185 | |||
186 | await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
187 | await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
188 | }) | ||
189 | |||
190 | it('Should not get files without appropriate password or appropriate OAuth token', async function () { | ||
191 | this.timeout(60000) | ||
192 | |||
193 | const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID) | ||
194 | |||
195 | await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
196 | await makeRawRequest({ | ||
197 | url: webVideoFile, | ||
198 | token: null, | ||
199 | headers: incorrectPasswordHeader, | ||
200 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
201 | }) | ||
202 | await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
203 | await makeRawRequest({ | ||
204 | url: webVideoFile, | ||
205 | token: null, | ||
206 | headers: correctPasswordHeader, | ||
207 | expectedStatus: HttpStatusCode.OK_200 | ||
208 | }) | ||
209 | |||
210 | await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
211 | await makeRawRequest({ | ||
212 | url: hlsFile, | ||
213 | token: null, | ||
214 | headers: incorrectPasswordHeader, | ||
215 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
216 | }) | ||
217 | await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
218 | await makeRawRequest({ | ||
219 | url: hlsFile, | ||
220 | token: null, | ||
221 | headers: correctPasswordHeader, | ||
222 | expectedStatus: HttpStatusCode.OK_200 | ||
223 | }) | ||
224 | }) | ||
225 | |||
226 | it('Should not get HLS file of another video', async function () { | ||
227 | this.timeout(60000) | ||
228 | |||
229 | const privateVideo = await server.videos.getWithToken({ id: privateVideoUUID }) | ||
230 | const hlsFilename = basename(getHLS(privateVideo).files[0].fileUrl) | ||
231 | |||
232 | const badUrl = server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + userPrivateVideoUUID + '/' + hlsFilename | ||
233 | const goodUrl = server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + privateVideoUUID + '/' + hlsFilename | ||
234 | |||
235 | await makeRawRequest({ url: badUrl, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
236 | await makeRawRequest({ url: goodUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
237 | }) | ||
238 | |||
239 | it('Should correctly check OAuth, video file token of private video', async function () { | ||
240 | this.timeout(60000) | ||
241 | |||
242 | const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID }) | ||
243 | const goodVideoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID }) | ||
244 | |||
245 | const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID) | ||
246 | |||
247 | for (const url of [ webVideoFile, hlsFile ]) { | ||
248 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
249 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
250 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
251 | |||
252 | await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
253 | await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
254 | |||
255 | } | ||
256 | }) | ||
257 | |||
258 | it('Should correctly check OAuth, video file token or video password of password protected video', async function () { | ||
259 | this.timeout(60000) | ||
260 | |||
261 | const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID }) | ||
262 | const goodVideoFileToken = await server.videoToken.getVideoFileToken({ | ||
263 | videoId: passwordProtectedVideoUUID, | ||
264 | videoPassword: correctPassword | ||
265 | }) | ||
266 | |||
267 | const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID) | ||
268 | |||
269 | for (const url of [ hlsFile, webVideoFile ]) { | ||
270 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
271 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
272 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
273 | |||
274 | await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
275 | await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
276 | |||
277 | await makeRawRequest({ | ||
278 | url, | ||
279 | headers: incorrectPasswordHeader, | ||
280 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
281 | }) | ||
282 | await makeRawRequest({ url, headers: correctPasswordHeader, expectedStatus: HttpStatusCode.OK_200 }) | ||
283 | } | ||
284 | }) | ||
285 | |||
286 | it('Should reinject video file token', async function () { | ||
287 | this.timeout(120000) | ||
288 | |||
289 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID }) | ||
290 | |||
291 | await checkVideoFileTokenReinjection({ | ||
292 | server, | ||
293 | videoUUID: privateVideoUUID, | ||
294 | videoFileToken, | ||
295 | resolutions: [ 240, 720 ], | ||
296 | isLive: false | ||
297 | }) | ||
298 | }) | ||
299 | |||
300 | it('Should update public video to private', async function () { | ||
301 | this.timeout(60000) | ||
302 | |||
303 | await server.videos.update({ id: publicVideoUUID, attributes: { privacy: VideoPrivacy.INTERNAL } }) | ||
304 | |||
305 | await checkPrivateVODFiles(publicVideoUUID) | ||
306 | }) | ||
307 | |||
308 | it('Should update private video to public', async function () { | ||
309 | this.timeout(60000) | ||
310 | |||
311 | await server.videos.update({ id: publicVideoUUID, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
312 | |||
313 | await checkPublicVODFiles(publicVideoUUID) | ||
314 | }) | ||
315 | }) | ||
316 | |||
317 | describe('Live', function () { | ||
318 | let normalLiveId: string | ||
319 | let normalLive: LiveVideo | ||
320 | |||
321 | let permanentLiveId: string | ||
322 | let permanentLive: LiveVideo | ||
323 | |||
324 | let passwordProtectedLiveId: string | ||
325 | let passwordProtectedLive: LiveVideo | ||
326 | |||
327 | const correctPassword = 'my super password' | ||
328 | |||
329 | let unrelatedFileToken: string | ||
330 | |||
331 | // --------------------------------------------------------------------------- | ||
332 | |||
333 | async function checkLiveFiles (live: LiveVideo, liveId: string, videoPassword?: string) { | ||
334 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
335 | await server.live.waitUntilPublished({ videoId: liveId }) | ||
336 | |||
337 | const video = videoPassword | ||
338 | ? await server.videos.getWithPassword({ id: liveId, password: videoPassword }) | ||
339 | : await server.videos.getWithToken({ id: liveId }) | ||
340 | |||
341 | const fileToken = videoPassword | ||
342 | ? await server.videoToken.getVideoFileToken({ token: null, videoId: video.uuid, videoPassword }) | ||
343 | : await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
344 | |||
345 | const hls = video.streamingPlaylists[0] | ||
346 | |||
347 | for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { | ||
348 | expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/') | ||
349 | |||
350 | await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
351 | await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
352 | |||
353 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
354 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
355 | if (videoPassword) { | ||
356 | await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 }) | ||
357 | } | ||
358 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
359 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
360 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
361 | if (videoPassword) { | ||
362 | await makeRawRequest({ | ||
363 | url, | ||
364 | headers: { 'x-peertube-video-password': 'incorrectPassword' }, | ||
365 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
366 | }) | ||
367 | } | ||
368 | } | ||
369 | |||
370 | await stopFfmpeg(ffmpegCommand) | ||
371 | } | ||
372 | |||
373 | async function checkReplay (replay: VideoDetails) { | ||
374 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: replay.uuid }) | ||
375 | |||
376 | const hls = replay.streamingPlaylists[0] | ||
377 | expect(hls.files).to.not.have.lengthOf(0) | ||
378 | |||
379 | for (const file of hls.files) { | ||
380 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
381 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
382 | |||
383 | await makeRawRequest({ url: file.fileUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
384 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
385 | await makeRawRequest({ | ||
386 | url: file.fileUrl, | ||
387 | query: { videoFileToken: unrelatedFileToken }, | ||
388 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
389 | }) | ||
390 | } | ||
391 | |||
392 | for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { | ||
393 | expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/') | ||
394 | |||
395 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
396 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
397 | |||
398 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
399 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
400 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
401 | } | ||
402 | } | ||
403 | |||
404 | // --------------------------------------------------------------------------- | ||
405 | |||
406 | before(async function () { | ||
407 | await server.config.enableMinimumTranscoding() | ||
408 | |||
409 | const { uuid } = await server.videos.quickUpload({ name: 'another video' }) | ||
410 | unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
411 | |||
412 | await server.config.enableLive({ | ||
413 | allowReplay: true, | ||
414 | transcoding: true, | ||
415 | resolutions: 'min' | ||
416 | }) | ||
417 | |||
418 | { | ||
419 | const { video, live } = await server.live.quickCreate({ | ||
420 | saveReplay: true, | ||
421 | permanentLive: false, | ||
422 | privacy: VideoPrivacy.PRIVATE | ||
423 | }) | ||
424 | normalLiveId = video.uuid | ||
425 | normalLive = live | ||
426 | } | ||
427 | |||
428 | { | ||
429 | const { video, live } = await server.live.quickCreate({ | ||
430 | saveReplay: true, | ||
431 | permanentLive: true, | ||
432 | privacy: VideoPrivacy.PRIVATE | ||
433 | }) | ||
434 | permanentLiveId = video.uuid | ||
435 | permanentLive = live | ||
436 | } | ||
437 | |||
438 | { | ||
439 | const { video, live } = await server.live.quickCreate({ | ||
440 | saveReplay: false, | ||
441 | permanentLive: false, | ||
442 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
443 | videoPasswords: [ correctPassword ] | ||
444 | }) | ||
445 | passwordProtectedLiveId = video.uuid | ||
446 | passwordProtectedLive = live | ||
447 | } | ||
448 | }) | ||
449 | |||
450 | it('Should create a private normal live and have a private static path', async function () { | ||
451 | this.timeout(240000) | ||
452 | |||
453 | await checkLiveFiles(normalLive, normalLiveId) | ||
454 | }) | ||
455 | |||
456 | it('Should create a private permanent live and have a private static path', async function () { | ||
457 | this.timeout(240000) | ||
458 | |||
459 | await checkLiveFiles(permanentLive, permanentLiveId) | ||
460 | }) | ||
461 | |||
462 | it('Should create a password protected live and have a private static path', async function () { | ||
463 | this.timeout(240000) | ||
464 | |||
465 | await checkLiveFiles(passwordProtectedLive, passwordProtectedLiveId, correctPassword) | ||
466 | }) | ||
467 | |||
468 | it('Should reinject video file token in permanent live', async function () { | ||
469 | this.timeout(240000) | ||
470 | |||
471 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey }) | ||
472 | await server.live.waitUntilPublished({ videoId: permanentLiveId }) | ||
473 | |||
474 | const video = await server.videos.getWithToken({ id: permanentLiveId }) | ||
475 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
476 | |||
477 | await checkVideoFileTokenReinjection({ | ||
478 | server, | ||
479 | videoUUID: permanentLiveId, | ||
480 | videoFileToken, | ||
481 | resolutions: [ 720 ], | ||
482 | isLive: true | ||
483 | }) | ||
484 | |||
485 | await stopFfmpeg(ffmpegCommand) | ||
486 | }) | ||
487 | |||
488 | it('Should have created a replay of the normal live with a private static path', async function () { | ||
489 | this.timeout(240000) | ||
490 | |||
491 | await server.live.waitUntilReplacedByReplay({ videoId: normalLiveId }) | ||
492 | |||
493 | const replay = await server.videos.getWithToken({ id: normalLiveId }) | ||
494 | await checkReplay(replay) | ||
495 | }) | ||
496 | |||
497 | it('Should have created a replay of the permanent live with a private static path', async function () { | ||
498 | this.timeout(240000) | ||
499 | |||
500 | await server.live.waitUntilWaiting({ videoId: permanentLiveId }) | ||
501 | await waitJobs([ server ]) | ||
502 | |||
503 | const live = await server.videos.getWithToken({ id: permanentLiveId }) | ||
504 | const replayFromList = await findExternalSavedVideo(server, live) | ||
505 | const replay = await server.videos.getWithToken({ id: replayFromList.id }) | ||
506 | |||
507 | await checkReplay(replay) | ||
508 | }) | ||
509 | }) | ||
510 | |||
511 | describe('With private files proxy disabled and public ACL for private files', function () { | ||
512 | let videoUUID: string | ||
513 | |||
514 | before(async function () { | ||
515 | this.timeout(240000) | ||
516 | |||
517 | await server.kill() | ||
518 | |||
519 | const config = ObjectStorageCommand.getDefaultScalewayConfig({ | ||
520 | serverNumber: 1, | ||
521 | enablePrivateProxy: false, | ||
522 | privateACL: 'public-read' | ||
523 | }) | ||
524 | await server.run(config) | ||
525 | |||
526 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
527 | videoUUID = uuid | ||
528 | |||
529 | await waitJobs([ server ]) | ||
530 | }) | ||
531 | |||
532 | it('Should display object storage path for a private video and be able to access them', async function () { | ||
533 | this.timeout(60000) | ||
534 | |||
535 | await checkPublicVODFiles(videoUUID) | ||
536 | }) | ||
537 | |||
538 | it('Should not be able to access object storage proxy', async function () { | ||
539 | const privateVideo = await server.videos.getWithToken({ id: videoUUID }) | ||
540 | const webVideoFilename = extractFilenameFromUrl(privateVideo.files[0].fileUrl) | ||
541 | const hlsFilename = extractFilenameFromUrl(getHLS(privateVideo).files[0].fileUrl) | ||
542 | |||
543 | await makeRawRequest({ | ||
544 | url: server.url + '/object-storage-proxy/web-videos/private/' + webVideoFilename, | ||
545 | token: server.accessToken, | ||
546 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
547 | }) | ||
548 | |||
549 | await makeRawRequest({ | ||
550 | url: server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + videoUUID + '/' + hlsFilename, | ||
551 | token: server.accessToken, | ||
552 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
553 | }) | ||
554 | }) | ||
555 | }) | ||
556 | |||
557 | after(async function () { | ||
558 | this.timeout(240000) | ||
559 | |||
560 | const { data } = await server.videos.listAllForAdmin() | ||
561 | |||
562 | for (const v of data) { | ||
563 | await server.videos.remove({ id: v.uuid }) | ||
564 | } | ||
565 | |||
566 | for (const v of data) { | ||
567 | await server.servers.waitUntilLog('Removed files of video ' + v.url) | ||
568 | } | ||
569 | |||
570 | await sqlCommand.cleanup() | ||
571 | await cleanupTests([ server ]) | ||
572 | }) | ||
573 | }) | ||
diff --git a/packages/tests/src/api/object-storage/videos.ts b/packages/tests/src/api/object-storage/videos.ts new file mode 100644 index 000000000..66bca5cc8 --- /dev/null +++ b/packages/tests/src/api/object-storage/videos.ts | |||
@@ -0,0 +1,434 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import bytes from 'bytes' | ||
4 | import { expect } from 'chai' | ||
5 | import { stat } from 'fs/promises' | ||
6 | import merge from 'lodash-es/merge.js' | ||
7 | import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models' | ||
8 | import { areMockObjectStorageTestsDisabled, sha1 } from '@peertube/peertube-node-utils' | ||
9 | import { | ||
10 | cleanupTests, | ||
11 | createMultipleServers, | ||
12 | createSingleServer, | ||
13 | doubleFollow, | ||
14 | killallServers, | ||
15 | makeRawRequest, | ||
16 | ObjectStorageCommand, | ||
17 | PeerTubeServer, | ||
18 | setAccessTokensToServers, | ||
19 | waitJobs | ||
20 | } from '@peertube/peertube-server-commands' | ||
21 | import { expectStartWith, expectLogDoesNotContain } from '@tests/shared/checks.js' | ||
22 | import { checkTmpIsEmpty } from '@tests/shared/directories.js' | ||
23 | import { generateHighBitrateVideo } from '@tests/shared/generate.js' | ||
24 | import { MockObjectStorageProxy } from '@tests/shared/mock-servers/mock-object-storage.js' | ||
25 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
26 | import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js' | ||
27 | |||
28 | async function checkFiles (options: { | ||
29 | server: PeerTubeServer | ||
30 | originServer: PeerTubeServer | ||
31 | originSQLCommand: SQLCommand | ||
32 | |||
33 | video: VideoDetails | ||
34 | |||
35 | baseMockUrl?: string | ||
36 | |||
37 | playlistBucket: string | ||
38 | playlistPrefix?: string | ||
39 | |||
40 | webVideoBucket: string | ||
41 | webVideoPrefix?: string | ||
42 | }) { | ||
43 | const { | ||
44 | server, | ||
45 | originServer, | ||
46 | originSQLCommand, | ||
47 | video, | ||
48 | playlistBucket, | ||
49 | webVideoBucket, | ||
50 | baseMockUrl, | ||
51 | playlistPrefix, | ||
52 | webVideoPrefix | ||
53 | } = options | ||
54 | |||
55 | let allFiles = video.files | ||
56 | |||
57 | for (const file of video.files) { | ||
58 | const baseUrl = baseMockUrl | ||
59 | ? `${baseMockUrl}/${webVideoBucket}/` | ||
60 | : `http://${webVideoBucket}.${ObjectStorageCommand.getMockEndpointHost()}/` | ||
61 | |||
62 | const prefix = webVideoPrefix || '' | ||
63 | const start = baseUrl + prefix | ||
64 | |||
65 | expectStartWith(file.fileUrl, start) | ||
66 | |||
67 | const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 }) | ||
68 | const location = res.headers['location'] | ||
69 | expectStartWith(location, start) | ||
70 | |||
71 | await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 }) | ||
72 | } | ||
73 | |||
74 | const hls = video.streamingPlaylists[0] | ||
75 | |||
76 | if (hls) { | ||
77 | allFiles = allFiles.concat(hls.files) | ||
78 | |||
79 | const baseUrl = baseMockUrl | ||
80 | ? `${baseMockUrl}/${playlistBucket}/` | ||
81 | : `http://${playlistBucket}.${ObjectStorageCommand.getMockEndpointHost()}/` | ||
82 | |||
83 | const prefix = playlistPrefix || '' | ||
84 | const start = baseUrl + prefix | ||
85 | |||
86 | expectStartWith(hls.playlistUrl, start) | ||
87 | expectStartWith(hls.segmentsSha256Url, start) | ||
88 | |||
89 | await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
90 | |||
91 | const resSha = await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
92 | expect(JSON.stringify(resSha.body)).to.not.throw | ||
93 | |||
94 | let i = 0 | ||
95 | for (const file of hls.files) { | ||
96 | expectStartWith(file.fileUrl, start) | ||
97 | |||
98 | const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 }) | ||
99 | const location = res.headers['location'] | ||
100 | expectStartWith(location, start) | ||
101 | |||
102 | await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 }) | ||
103 | |||
104 | if (originServer.internalServerNumber === server.internalServerNumber) { | ||
105 | const infohash = sha1(`${2 + hls.playlistUrl}+V${i}`) | ||
106 | const dbInfohashes = await originSQLCommand.getPlaylistInfohash(hls.id) | ||
107 | |||
108 | expect(dbInfohashes).to.include(infohash) | ||
109 | } | ||
110 | |||
111 | i++ | ||
112 | } | ||
113 | } | ||
114 | |||
115 | for (const file of allFiles) { | ||
116 | await checkWebTorrentWorks(file.magnetUri) | ||
117 | |||
118 | const res = await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
119 | expect(res.body).to.have.length.above(100) | ||
120 | } | ||
121 | |||
122 | return allFiles.map(f => f.fileUrl) | ||
123 | } | ||
124 | |||
125 | function runTestSuite (options: { | ||
126 | fixture?: string | ||
127 | |||
128 | maxUploadPart?: string | ||
129 | |||
130 | playlistBucket: string | ||
131 | playlistPrefix?: string | ||
132 | |||
133 | webVideoBucket: string | ||
134 | webVideoPrefix?: string | ||
135 | |||
136 | useMockBaseUrl?: boolean | ||
137 | }) { | ||
138 | const mockObjectStorageProxy = new MockObjectStorageProxy() | ||
139 | const { fixture } = options | ||
140 | let baseMockUrl: string | ||
141 | |||
142 | let servers: PeerTubeServer[] | ||
143 | let sqlCommands: SQLCommand[] = [] | ||
144 | const objectStorage = new ObjectStorageCommand() | ||
145 | |||
146 | let keptUrls: string[] = [] | ||
147 | |||
148 | const uuidsToDelete: string[] = [] | ||
149 | let deletedUrls: string[] = [] | ||
150 | |||
151 | before(async function () { | ||
152 | this.timeout(240000) | ||
153 | |||
154 | const port = await mockObjectStorageProxy.initialize() | ||
155 | baseMockUrl = options.useMockBaseUrl | ||
156 | ? `http://127.0.0.1:${port}` | ||
157 | : undefined | ||
158 | |||
159 | await objectStorage.createMockBucket(options.playlistBucket) | ||
160 | await objectStorage.createMockBucket(options.webVideoBucket) | ||
161 | |||
162 | const config = { | ||
163 | object_storage: { | ||
164 | enabled: true, | ||
165 | endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(), | ||
166 | region: ObjectStorageCommand.getMockRegion(), | ||
167 | |||
168 | credentials: ObjectStorageCommand.getMockCredentialsConfig(), | ||
169 | |||
170 | max_upload_part: options.maxUploadPart || '5MB', | ||
171 | |||
172 | streaming_playlists: { | ||
173 | bucket_name: options.playlistBucket, | ||
174 | prefix: options.playlistPrefix, | ||
175 | base_url: baseMockUrl | ||
176 | ? `${baseMockUrl}/${options.playlistBucket}` | ||
177 | : undefined | ||
178 | }, | ||
179 | |||
180 | web_videos: { | ||
181 | bucket_name: options.webVideoBucket, | ||
182 | prefix: options.webVideoPrefix, | ||
183 | base_url: baseMockUrl | ||
184 | ? `${baseMockUrl}/${options.webVideoBucket}` | ||
185 | : undefined | ||
186 | } | ||
187 | } | ||
188 | } | ||
189 | |||
190 | servers = await createMultipleServers(2, config) | ||
191 | |||
192 | await setAccessTokensToServers(servers) | ||
193 | await doubleFollow(servers[0], servers[1]) | ||
194 | |||
195 | for (const server of servers) { | ||
196 | const { uuid } = await server.videos.quickUpload({ name: 'video to keep' }) | ||
197 | await waitJobs(servers) | ||
198 | |||
199 | const files = await server.videos.listFiles({ id: uuid }) | ||
200 | keptUrls = keptUrls.concat(files.map(f => f.fileUrl)) | ||
201 | } | ||
202 | |||
203 | sqlCommands = servers.map(s => new SQLCommand(s)) | ||
204 | }) | ||
205 | |||
206 | it('Should upload a video and move it to the object storage without transcoding', async function () { | ||
207 | this.timeout(40000) | ||
208 | |||
209 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1', fixture }) | ||
210 | uuidsToDelete.push(uuid) | ||
211 | |||
212 | await waitJobs(servers) | ||
213 | |||
214 | for (const server of servers) { | ||
215 | const video = await server.videos.get({ id: uuid }) | ||
216 | const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl }) | ||
217 | |||
218 | deletedUrls = deletedUrls.concat(files) | ||
219 | } | ||
220 | }) | ||
221 | |||
222 | it('Should upload a video and move it to the object storage with transcoding', async function () { | ||
223 | this.timeout(120000) | ||
224 | |||
225 | const { uuid } = await servers[1].videos.quickUpload({ name: 'video 2', fixture }) | ||
226 | uuidsToDelete.push(uuid) | ||
227 | |||
228 | await waitJobs(servers) | ||
229 | |||
230 | for (const server of servers) { | ||
231 | const video = await server.videos.get({ id: uuid }) | ||
232 | const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl }) | ||
233 | |||
234 | deletedUrls = deletedUrls.concat(files) | ||
235 | } | ||
236 | }) | ||
237 | |||
238 | it('Should fetch correctly all the files', async function () { | ||
239 | for (const url of deletedUrls.concat(keptUrls)) { | ||
240 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) | ||
241 | } | ||
242 | }) | ||
243 | |||
244 | it('Should correctly delete the files', async function () { | ||
245 | await servers[0].videos.remove({ id: uuidsToDelete[0] }) | ||
246 | await servers[1].videos.remove({ id: uuidsToDelete[1] }) | ||
247 | |||
248 | await waitJobs(servers) | ||
249 | |||
250 | for (const url of deletedUrls) { | ||
251 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
252 | } | ||
253 | }) | ||
254 | |||
255 | it('Should have kept other files', async function () { | ||
256 | for (const url of keptUrls) { | ||
257 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) | ||
258 | } | ||
259 | }) | ||
260 | |||
261 | it('Should have an empty tmp directory', async function () { | ||
262 | for (const server of servers) { | ||
263 | await checkTmpIsEmpty(server) | ||
264 | } | ||
265 | }) | ||
266 | |||
267 | it('Should not have downloaded files from object storage', async function () { | ||
268 | for (const server of servers) { | ||
269 | await expectLogDoesNotContain(server, 'from object storage') | ||
270 | } | ||
271 | }) | ||
272 | |||
273 | after(async function () { | ||
274 | await mockObjectStorageProxy.terminate() | ||
275 | await objectStorage.cleanupMock() | ||
276 | |||
277 | for (const sqlCommand of sqlCommands) { | ||
278 | await sqlCommand.cleanup() | ||
279 | } | ||
280 | |||
281 | await cleanupTests(servers) | ||
282 | }) | ||
283 | } | ||
284 | |||
285 | describe('Object storage for videos', function () { | ||
286 | if (areMockObjectStorageTestsDisabled()) return | ||
287 | |||
288 | const objectStorage = new ObjectStorageCommand() | ||
289 | |||
290 | describe('Test config', function () { | ||
291 | let server: PeerTubeServer | ||
292 | |||
293 | const baseConfig = objectStorage.getDefaultMockConfig() | ||
294 | |||
295 | const badCredentials = { | ||
296 | access_key_id: 'AKIAIOSFODNN7EXAMPLE', | ||
297 | secret_access_key: 'aJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' | ||
298 | } | ||
299 | |||
300 | it('Should fail with same bucket names without prefix', function (done) { | ||
301 | const config = merge({}, baseConfig, { | ||
302 | object_storage: { | ||
303 | streaming_playlists: { | ||
304 | bucket_name: 'aaa' | ||
305 | }, | ||
306 | |||
307 | web_videos: { | ||
308 | bucket_name: 'aaa' | ||
309 | } | ||
310 | } | ||
311 | }) | ||
312 | |||
313 | createSingleServer(1, config) | ||
314 | .then(() => done(new Error('Did not throw'))) | ||
315 | .catch(() => done()) | ||
316 | }) | ||
317 | |||
318 | it('Should fail with bad credentials', async function () { | ||
319 | this.timeout(60000) | ||
320 | |||
321 | await objectStorage.prepareDefaultMockBuckets() | ||
322 | |||
323 | const config = merge({}, baseConfig, { | ||
324 | object_storage: { | ||
325 | credentials: badCredentials | ||
326 | } | ||
327 | }) | ||
328 | |||
329 | server = await createSingleServer(1, config) | ||
330 | await setAccessTokensToServers([ server ]) | ||
331 | |||
332 | const { uuid } = await server.videos.quickUpload({ name: 'video' }) | ||
333 | |||
334 | await waitJobs([ server ], { skipDelayed: true }) | ||
335 | const video = await server.videos.get({ id: uuid }) | ||
336 | |||
337 | expectStartWith(video.files[0].fileUrl, server.url) | ||
338 | |||
339 | await killallServers([ server ]) | ||
340 | }) | ||
341 | |||
342 | it('Should succeed with credentials from env', async function () { | ||
343 | this.timeout(60000) | ||
344 | |||
345 | await objectStorage.prepareDefaultMockBuckets() | ||
346 | |||
347 | const config = merge({}, baseConfig, { | ||
348 | object_storage: { | ||
349 | credentials: { | ||
350 | access_key_id: '', | ||
351 | secret_access_key: '' | ||
352 | } | ||
353 | } | ||
354 | }) | ||
355 | |||
356 | const goodCredentials = ObjectStorageCommand.getMockCredentialsConfig() | ||
357 | |||
358 | server = await createSingleServer(1, config, { | ||
359 | env: { | ||
360 | AWS_ACCESS_KEY_ID: goodCredentials.access_key_id, | ||
361 | AWS_SECRET_ACCESS_KEY: goodCredentials.secret_access_key | ||
362 | } | ||
363 | }) | ||
364 | |||
365 | await setAccessTokensToServers([ server ]) | ||
366 | |||
367 | const { uuid } = await server.videos.quickUpload({ name: 'video' }) | ||
368 | |||
369 | await waitJobs([ server ], { skipDelayed: true }) | ||
370 | const video = await server.videos.get({ id: uuid }) | ||
371 | |||
372 | expectStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
373 | }) | ||
374 | |||
375 | after(async function () { | ||
376 | await objectStorage.cleanupMock() | ||
377 | |||
378 | await cleanupTests([ server ]) | ||
379 | }) | ||
380 | }) | ||
381 | |||
382 | describe('Test simple object storage', function () { | ||
383 | runTestSuite({ | ||
384 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), | ||
385 | webVideoBucket: objectStorage.getMockBucketName('web-videos') | ||
386 | }) | ||
387 | }) | ||
388 | |||
389 | describe('Test object storage with prefix', function () { | ||
390 | runTestSuite({ | ||
391 | playlistBucket: objectStorage.getMockBucketName('mybucket'), | ||
392 | webVideoBucket: objectStorage.getMockBucketName('mybucket'), | ||
393 | |||
394 | playlistPrefix: 'streaming-playlists_', | ||
395 | webVideoPrefix: 'webvideo_' | ||
396 | }) | ||
397 | }) | ||
398 | |||
399 | describe('Test object storage with prefix and base URL', function () { | ||
400 | runTestSuite({ | ||
401 | playlistBucket: objectStorage.getMockBucketName('mybucket'), | ||
402 | webVideoBucket: objectStorage.getMockBucketName('mybucket'), | ||
403 | |||
404 | playlistPrefix: 'streaming-playlists/', | ||
405 | webVideoPrefix: 'webvideo/', | ||
406 | |||
407 | useMockBaseUrl: true | ||
408 | }) | ||
409 | }) | ||
410 | |||
411 | describe('Test object storage with file bigger than upload part', function () { | ||
412 | let fixture: string | ||
413 | const maxUploadPart = '5MB' | ||
414 | |||
415 | before(async function () { | ||
416 | this.timeout(120000) | ||
417 | |||
418 | fixture = await generateHighBitrateVideo() | ||
419 | |||
420 | const { size } = await stat(fixture) | ||
421 | |||
422 | if (bytes.parse(maxUploadPart) > size) { | ||
423 | throw Error(`Fixture file is too small (${size}) to make sense for this test.`) | ||
424 | } | ||
425 | }) | ||
426 | |||
427 | runTestSuite({ | ||
428 | maxUploadPart, | ||
429 | playlistBucket: objectStorage.getMockBucketName('streaming-playlists'), | ||
430 | webVideoBucket: objectStorage.getMockBucketName('web-videos'), | ||
431 | fixture | ||
432 | }) | ||
433 | }) | ||
434 | }) | ||
diff --git a/packages/tests/src/api/redundancy/index.ts b/packages/tests/src/api/redundancy/index.ts new file mode 100644 index 000000000..f6b70c8af --- /dev/null +++ b/packages/tests/src/api/redundancy/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | import './redundancy-constraints.js' | ||
2 | import './redundancy.js' | ||
3 | import './manage-redundancy.js' | ||
diff --git a/packages/tests/src/api/redundancy/manage-redundancy.ts b/packages/tests/src/api/redundancy/manage-redundancy.ts new file mode 100644 index 000000000..14556e26c --- /dev/null +++ b/packages/tests/src/api/redundancy/manage-redundancy.ts | |||
@@ -0,0 +1,324 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | PeerTubeServer, | ||
9 | RedundancyCommand, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { VideoPrivacy, VideoRedundanciesTarget } from '@peertube/peertube-models' | ||
14 | |||
15 | describe('Test manage videos redundancy', function () { | ||
16 | const targets: VideoRedundanciesTarget[] = [ 'my-videos', 'remote-videos' ] | ||
17 | |||
18 | let servers: PeerTubeServer[] | ||
19 | let video1Server2UUID: string | ||
20 | let video2Server2UUID: string | ||
21 | let redundanciesToRemove: number[] = [] | ||
22 | |||
23 | let commands: RedundancyCommand[] | ||
24 | |||
25 | before(async function () { | ||
26 | this.timeout(120000) | ||
27 | |||
28 | const config = { | ||
29 | transcoding: { | ||
30 | hls: { | ||
31 | enabled: true | ||
32 | } | ||
33 | }, | ||
34 | redundancy: { | ||
35 | videos: { | ||
36 | check_interval: '1 second', | ||
37 | strategies: [ | ||
38 | { | ||
39 | strategy: 'recently-added', | ||
40 | min_lifetime: '1 hour', | ||
41 | size: '10MB', | ||
42 | min_views: 0 | ||
43 | } | ||
44 | ] | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | servers = await createMultipleServers(3, config) | ||
49 | |||
50 | // Get the access tokens | ||
51 | await setAccessTokensToServers(servers) | ||
52 | |||
53 | commands = servers.map(s => s.redundancy) | ||
54 | |||
55 | { | ||
56 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } }) | ||
57 | video1Server2UUID = uuid | ||
58 | } | ||
59 | |||
60 | { | ||
61 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2' } }) | ||
62 | video2Server2UUID = uuid | ||
63 | } | ||
64 | |||
65 | await waitJobs(servers) | ||
66 | |||
67 | // Server 1 and server 2 follow each other | ||
68 | await doubleFollow(servers[0], servers[1]) | ||
69 | await doubleFollow(servers[0], servers[2]) | ||
70 | await commands[0].updateRedundancy({ host: servers[1].host, redundancyAllowed: true }) | ||
71 | |||
72 | await waitJobs(servers) | ||
73 | }) | ||
74 | |||
75 | it('Should not have redundancies on server 3', async function () { | ||
76 | for (const target of targets) { | ||
77 | const body = await commands[2].listVideos({ target }) | ||
78 | |||
79 | expect(body.total).to.equal(0) | ||
80 | expect(body.data).to.have.lengthOf(0) | ||
81 | } | ||
82 | }) | ||
83 | |||
84 | it('Should correctly list followings by redundancy', async function () { | ||
85 | const body = await servers[0].follows.getFollowings({ sort: '-redundancyAllowed' }) | ||
86 | |||
87 | expect(body.total).to.equal(2) | ||
88 | expect(body.data).to.have.lengthOf(2) | ||
89 | |||
90 | expect(body.data[0].following.host).to.equal(servers[1].host) | ||
91 | expect(body.data[1].following.host).to.equal(servers[2].host) | ||
92 | }) | ||
93 | |||
94 | it('Should not have "remote-videos" redundancies on server 2', async function () { | ||
95 | this.timeout(120000) | ||
96 | |||
97 | await waitJobs(servers) | ||
98 | await servers[0].servers.waitUntilLog('Duplicated ', 10) | ||
99 | await waitJobs(servers) | ||
100 | |||
101 | const body = await commands[1].listVideos({ target: 'remote-videos' }) | ||
102 | |||
103 | expect(body.total).to.equal(0) | ||
104 | expect(body.data).to.have.lengthOf(0) | ||
105 | }) | ||
106 | |||
107 | it('Should have "my-videos" redundancies on server 2', async function () { | ||
108 | this.timeout(120000) | ||
109 | |||
110 | const body = await commands[1].listVideos({ target: 'my-videos' }) | ||
111 | expect(body.total).to.equal(2) | ||
112 | |||
113 | const videos = body.data | ||
114 | expect(videos).to.have.lengthOf(2) | ||
115 | |||
116 | const videos1 = videos.find(v => v.uuid === video1Server2UUID) | ||
117 | const videos2 = videos.find(v => v.uuid === video2Server2UUID) | ||
118 | |||
119 | expect(videos1.name).to.equal('video 1 server 2') | ||
120 | expect(videos2.name).to.equal('video 2 server 2') | ||
121 | |||
122 | expect(videos1.redundancies.files).to.have.lengthOf(4) | ||
123 | expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
124 | |||
125 | const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists) | ||
126 | |||
127 | for (const r of redundancies) { | ||
128 | expect(r.strategy).to.be.null | ||
129 | expect(r.fileUrl).to.exist | ||
130 | expect(r.createdAt).to.exist | ||
131 | expect(r.updatedAt).to.exist | ||
132 | expect(r.expiresOn).to.exist | ||
133 | } | ||
134 | }) | ||
135 | |||
136 | it('Should not have "my-videos" redundancies on server 1', async function () { | ||
137 | const body = await commands[0].listVideos({ target: 'my-videos' }) | ||
138 | |||
139 | expect(body.total).to.equal(0) | ||
140 | expect(body.data).to.have.lengthOf(0) | ||
141 | }) | ||
142 | |||
143 | it('Should have "remote-videos" redundancies on server 1', async function () { | ||
144 | this.timeout(120000) | ||
145 | |||
146 | const body = await commands[0].listVideos({ target: 'remote-videos' }) | ||
147 | expect(body.total).to.equal(2) | ||
148 | |||
149 | const videos = body.data | ||
150 | expect(videos).to.have.lengthOf(2) | ||
151 | |||
152 | const videos1 = videos.find(v => v.uuid === video1Server2UUID) | ||
153 | const videos2 = videos.find(v => v.uuid === video2Server2UUID) | ||
154 | |||
155 | expect(videos1.name).to.equal('video 1 server 2') | ||
156 | expect(videos2.name).to.equal('video 2 server 2') | ||
157 | |||
158 | expect(videos1.redundancies.files).to.have.lengthOf(4) | ||
159 | expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
160 | |||
161 | const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists) | ||
162 | |||
163 | for (const r of redundancies) { | ||
164 | expect(r.strategy).to.equal('recently-added') | ||
165 | expect(r.fileUrl).to.exist | ||
166 | expect(r.createdAt).to.exist | ||
167 | expect(r.updatedAt).to.exist | ||
168 | expect(r.expiresOn).to.exist | ||
169 | } | ||
170 | }) | ||
171 | |||
172 | it('Should correctly paginate and sort results', async function () { | ||
173 | { | ||
174 | const body = await commands[0].listVideos({ | ||
175 | target: 'remote-videos', | ||
176 | sort: 'name', | ||
177 | start: 0, | ||
178 | count: 2 | ||
179 | }) | ||
180 | |||
181 | const videos = body.data | ||
182 | expect(videos[0].name).to.equal('video 1 server 2') | ||
183 | expect(videos[1].name).to.equal('video 2 server 2') | ||
184 | } | ||
185 | |||
186 | { | ||
187 | const body = await commands[0].listVideos({ | ||
188 | target: 'remote-videos', | ||
189 | sort: '-name', | ||
190 | start: 0, | ||
191 | count: 2 | ||
192 | }) | ||
193 | |||
194 | const videos = body.data | ||
195 | expect(videos[0].name).to.equal('video 2 server 2') | ||
196 | expect(videos[1].name).to.equal('video 1 server 2') | ||
197 | } | ||
198 | |||
199 | { | ||
200 | const body = await commands[0].listVideos({ | ||
201 | target: 'remote-videos', | ||
202 | sort: '-name', | ||
203 | start: 1, | ||
204 | count: 1 | ||
205 | }) | ||
206 | |||
207 | expect(body.data[0].name).to.equal('video 1 server 2') | ||
208 | } | ||
209 | }) | ||
210 | |||
211 | it('Should manually add a redundancy and list it', async function () { | ||
212 | this.timeout(120000) | ||
213 | |||
214 | const uuid = (await servers[1].videos.quickUpload({ name: 'video 3 server 2', privacy: VideoPrivacy.UNLISTED })).uuid | ||
215 | await waitJobs(servers) | ||
216 | const videoId = await servers[0].videos.getId({ uuid }) | ||
217 | |||
218 | await commands[0].addVideo({ videoId }) | ||
219 | |||
220 | await waitJobs(servers) | ||
221 | await servers[0].servers.waitUntilLog('Duplicated ', 15) | ||
222 | await waitJobs(servers) | ||
223 | |||
224 | { | ||
225 | const body = await commands[0].listVideos({ | ||
226 | target: 'remote-videos', | ||
227 | sort: '-name', | ||
228 | start: 0, | ||
229 | count: 5 | ||
230 | }) | ||
231 | |||
232 | const video = body.data[0] | ||
233 | |||
234 | expect(video.name).to.equal('video 3 server 2') | ||
235 | expect(video.redundancies.files).to.have.lengthOf(4) | ||
236 | expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
237 | |||
238 | const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists) | ||
239 | |||
240 | for (const r of redundancies) { | ||
241 | redundanciesToRemove.push(r.id) | ||
242 | |||
243 | expect(r.strategy).to.equal('manual') | ||
244 | expect(r.fileUrl).to.exist | ||
245 | expect(r.createdAt).to.exist | ||
246 | expect(r.updatedAt).to.exist | ||
247 | expect(r.expiresOn).to.be.null | ||
248 | } | ||
249 | } | ||
250 | |||
251 | const body = await commands[1].listVideos({ | ||
252 | target: 'my-videos', | ||
253 | sort: '-name', | ||
254 | start: 0, | ||
255 | count: 5 | ||
256 | }) | ||
257 | |||
258 | const video = body.data[0] | ||
259 | expect(video.name).to.equal('video 3 server 2') | ||
260 | expect(video.redundancies.files).to.have.lengthOf(4) | ||
261 | expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
262 | |||
263 | const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists) | ||
264 | |||
265 | for (const r of redundancies) { | ||
266 | expect(r.strategy).to.be.null | ||
267 | expect(r.fileUrl).to.exist | ||
268 | expect(r.createdAt).to.exist | ||
269 | expect(r.updatedAt).to.exist | ||
270 | expect(r.expiresOn).to.be.null | ||
271 | } | ||
272 | }) | ||
273 | |||
274 | it('Should manually remove a redundancy and remove it from the list', async function () { | ||
275 | this.timeout(120000) | ||
276 | |||
277 | for (const redundancyId of redundanciesToRemove) { | ||
278 | await commands[0].removeVideo({ redundancyId }) | ||
279 | } | ||
280 | |||
281 | { | ||
282 | const body = await commands[0].listVideos({ | ||
283 | target: 'remote-videos', | ||
284 | sort: '-name', | ||
285 | start: 0, | ||
286 | count: 5 | ||
287 | }) | ||
288 | |||
289 | const videos = body.data | ||
290 | |||
291 | expect(videos).to.have.lengthOf(2) | ||
292 | |||
293 | const video = videos[0] | ||
294 | expect(video.name).to.equal('video 2 server 2') | ||
295 | expect(video.redundancies.files).to.have.lengthOf(4) | ||
296 | expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1) | ||
297 | |||
298 | const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists) | ||
299 | |||
300 | redundanciesToRemove = redundancies.map(r => r.id) | ||
301 | } | ||
302 | }) | ||
303 | |||
304 | it('Should remove another (auto) redundancy', async function () { | ||
305 | for (const redundancyId of redundanciesToRemove) { | ||
306 | await commands[0].removeVideo({ redundancyId }) | ||
307 | } | ||
308 | |||
309 | const body = await commands[0].listVideos({ | ||
310 | target: 'remote-videos', | ||
311 | sort: '-name', | ||
312 | start: 0, | ||
313 | count: 5 | ||
314 | }) | ||
315 | |||
316 | const videos = body.data | ||
317 | expect(videos).to.have.lengthOf(1) | ||
318 | expect(videos[0].name).to.equal('video 1 server 2') | ||
319 | }) | ||
320 | |||
321 | after(async function () { | ||
322 | await cleanupTests(servers) | ||
323 | }) | ||
324 | }) | ||
diff --git a/packages/tests/src/api/redundancy/redundancy-constraints.ts b/packages/tests/src/api/redundancy/redundancy-constraints.ts new file mode 100644 index 000000000..24966b270 --- /dev/null +++ b/packages/tests/src/api/redundancy/redundancy-constraints.ts | |||
@@ -0,0 +1,191 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | killallServers, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test redundancy constraints', function () { | ||
15 | let remoteServer: PeerTubeServer | ||
16 | let localServer: PeerTubeServer | ||
17 | let servers: PeerTubeServer[] | ||
18 | |||
19 | const remoteServerConfig = { | ||
20 | redundancy: { | ||
21 | videos: { | ||
22 | check_interval: '1 second', | ||
23 | strategies: [ | ||
24 | { | ||
25 | strategy: 'recently-added', | ||
26 | min_lifetime: '1 hour', | ||
27 | size: '100MB', | ||
28 | min_views: 0 | ||
29 | } | ||
30 | ] | ||
31 | } | ||
32 | } | ||
33 | } | ||
34 | |||
35 | async function uploadWrapper (videoName: string) { | ||
36 | // Wait for transcoding | ||
37 | const { id } = await localServer.videos.upload({ attributes: { name: 'to transcode', privacy: VideoPrivacy.PRIVATE } }) | ||
38 | await waitJobs([ localServer ]) | ||
39 | |||
40 | // Update video to schedule a federation | ||
41 | await localServer.videos.update({ id, attributes: { name: videoName, privacy: VideoPrivacy.PUBLIC } }) | ||
42 | } | ||
43 | |||
44 | async function getTotalRedundanciesLocalServer () { | ||
45 | const body = await localServer.redundancy.listVideos({ target: 'my-videos' }) | ||
46 | |||
47 | return body.total | ||
48 | } | ||
49 | |||
50 | async function getTotalRedundanciesRemoteServer () { | ||
51 | const body = await remoteServer.redundancy.listVideos({ target: 'remote-videos' }) | ||
52 | |||
53 | return body.total | ||
54 | } | ||
55 | |||
56 | before(async function () { | ||
57 | this.timeout(120000) | ||
58 | |||
59 | { | ||
60 | remoteServer = await createSingleServer(1, remoteServerConfig) | ||
61 | } | ||
62 | |||
63 | { | ||
64 | const config = { | ||
65 | remote_redundancy: { | ||
66 | videos: { | ||
67 | accept_from: 'nobody' | ||
68 | } | ||
69 | } | ||
70 | } | ||
71 | localServer = await createSingleServer(2, config) | ||
72 | } | ||
73 | |||
74 | servers = [ remoteServer, localServer ] | ||
75 | |||
76 | // Get the access tokens | ||
77 | await setAccessTokensToServers(servers) | ||
78 | |||
79 | await localServer.videos.upload({ attributes: { name: 'video 1 server 2' } }) | ||
80 | |||
81 | await waitJobs(servers) | ||
82 | |||
83 | // Server 1 and server 2 follow each other | ||
84 | await remoteServer.follows.follow({ hosts: [ localServer.url ] }) | ||
85 | await waitJobs(servers) | ||
86 | await remoteServer.redundancy.updateRedundancy({ host: localServer.host, redundancyAllowed: true }) | ||
87 | |||
88 | await waitJobs(servers) | ||
89 | }) | ||
90 | |||
91 | it('Should have redundancy on server 1 but not on server 2 with a nobody filter', async function () { | ||
92 | this.timeout(120000) | ||
93 | |||
94 | await waitJobs(servers) | ||
95 | await remoteServer.servers.waitUntilLog('Duplicated ', 5) | ||
96 | await waitJobs(servers) | ||
97 | |||
98 | { | ||
99 | const total = await getTotalRedundanciesRemoteServer() | ||
100 | expect(total).to.equal(1) | ||
101 | } | ||
102 | |||
103 | { | ||
104 | const total = await getTotalRedundanciesLocalServer() | ||
105 | expect(total).to.equal(0) | ||
106 | } | ||
107 | }) | ||
108 | |||
109 | it('Should have redundancy on server 1 and on server 2 with an anybody filter', async function () { | ||
110 | this.timeout(120000) | ||
111 | |||
112 | const config = { | ||
113 | remote_redundancy: { | ||
114 | videos: { | ||
115 | accept_from: 'anybody' | ||
116 | } | ||
117 | } | ||
118 | } | ||
119 | await killallServers([ localServer ]) | ||
120 | await localServer.run(config) | ||
121 | |||
122 | await uploadWrapper('video 2 server 2') | ||
123 | |||
124 | await remoteServer.servers.waitUntilLog('Duplicated ', 10) | ||
125 | await waitJobs(servers) | ||
126 | |||
127 | { | ||
128 | const total = await getTotalRedundanciesRemoteServer() | ||
129 | expect(total).to.equal(2) | ||
130 | } | ||
131 | |||
132 | { | ||
133 | const total = await getTotalRedundanciesLocalServer() | ||
134 | expect(total).to.equal(1) | ||
135 | } | ||
136 | }) | ||
137 | |||
138 | it('Should have redundancy on server 1 but not on server 2 with a followings filter', async function () { | ||
139 | this.timeout(120000) | ||
140 | |||
141 | const config = { | ||
142 | remote_redundancy: { | ||
143 | videos: { | ||
144 | accept_from: 'followings' | ||
145 | } | ||
146 | } | ||
147 | } | ||
148 | await killallServers([ localServer ]) | ||
149 | await localServer.run(config) | ||
150 | |||
151 | await uploadWrapper('video 3 server 2') | ||
152 | |||
153 | await remoteServer.servers.waitUntilLog('Duplicated ', 15) | ||
154 | await waitJobs(servers) | ||
155 | |||
156 | { | ||
157 | const total = await getTotalRedundanciesRemoteServer() | ||
158 | expect(total).to.equal(3) | ||
159 | } | ||
160 | |||
161 | { | ||
162 | const total = await getTotalRedundanciesLocalServer() | ||
163 | expect(total).to.equal(1) | ||
164 | } | ||
165 | }) | ||
166 | |||
167 | it('Should have redundancy on server 1 and on server 2 with followings filter now server 2 follows server 1', async function () { | ||
168 | this.timeout(120000) | ||
169 | |||
170 | await localServer.follows.follow({ hosts: [ remoteServer.url ] }) | ||
171 | await waitJobs(servers) | ||
172 | |||
173 | await uploadWrapper('video 4 server 2') | ||
174 | await remoteServer.servers.waitUntilLog('Duplicated ', 20) | ||
175 | await waitJobs(servers) | ||
176 | |||
177 | { | ||
178 | const total = await getTotalRedundanciesRemoteServer() | ||
179 | expect(total).to.equal(4) | ||
180 | } | ||
181 | |||
182 | { | ||
183 | const total = await getTotalRedundanciesLocalServer() | ||
184 | expect(total).to.equal(2) | ||
185 | } | ||
186 | }) | ||
187 | |||
188 | after(async function () { | ||
189 | await cleanupTests(servers) | ||
190 | }) | ||
191 | }) | ||
diff --git a/packages/tests/src/api/redundancy/redundancy.ts b/packages/tests/src/api/redundancy/redundancy.ts new file mode 100644 index 000000000..69afae037 --- /dev/null +++ b/packages/tests/src/api/redundancy/redundancy.ts | |||
@@ -0,0 +1,743 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { readdir } from 'fs/promises' | ||
5 | import { decode as magnetUriDecode } from 'magnet-uri' | ||
6 | import { basename, join } from 'path' | ||
7 | import { wait } from '@peertube/peertube-core-utils' | ||
8 | import { | ||
9 | HttpStatusCode, | ||
10 | VideoDetails, | ||
11 | VideoFile, | ||
12 | VideoPrivacy, | ||
13 | VideoRedundancyStrategy, | ||
14 | VideoRedundancyStrategyWithManual | ||
15 | } from '@peertube/peertube-models' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | killallServers, | ||
21 | makeRawRequest, | ||
22 | PeerTubeServer, | ||
23 | setAccessTokensToServers, | ||
24 | waitJobs | ||
25 | } from '@peertube/peertube-server-commands' | ||
26 | import { checkSegmentHash } from '@tests/shared/streaming-playlists.js' | ||
27 | import { checkVideoFilesWereRemoved, saveVideoInServers } from '@tests/shared/videos.js' | ||
28 | |||
29 | let servers: PeerTubeServer[] = [] | ||
30 | let video1Server2: VideoDetails | ||
31 | |||
32 | async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], server: PeerTubeServer) { | ||
33 | const parsed = magnetUriDecode(file.magnetUri) | ||
34 | |||
35 | for (const ws of baseWebseeds) { | ||
36 | const found = parsed.urlList.find(url => url === `${ws}${basename(file.fileUrl)}`) | ||
37 | expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined | ||
38 | } | ||
39 | |||
40 | expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) | ||
41 | |||
42 | for (const url of parsed.urlList) { | ||
43 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) | ||
44 | } | ||
45 | } | ||
46 | |||
47 | async function createServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebVideo = true) { | ||
48 | const strategies: any[] = [] | ||
49 | |||
50 | if (strategy !== null) { | ||
51 | strategies.push( | ||
52 | { | ||
53 | min_lifetime: '1 hour', | ||
54 | strategy, | ||
55 | size: '400KB', | ||
56 | |||
57 | ...additionalParams | ||
58 | } | ||
59 | ) | ||
60 | } | ||
61 | |||
62 | const config = { | ||
63 | transcoding: { | ||
64 | web_videos: { | ||
65 | enabled: withWebVideo | ||
66 | }, | ||
67 | hls: { | ||
68 | enabled: true | ||
69 | } | ||
70 | }, | ||
71 | redundancy: { | ||
72 | videos: { | ||
73 | check_interval: '5 seconds', | ||
74 | strategies | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | |||
79 | servers = await createMultipleServers(3, config) | ||
80 | |||
81 | // Get the access tokens | ||
82 | await setAccessTokensToServers(servers) | ||
83 | |||
84 | { | ||
85 | const { id } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } }) | ||
86 | video1Server2 = await servers[1].videos.get({ id }) | ||
87 | |||
88 | await servers[1].views.simulateView({ id }) | ||
89 | } | ||
90 | |||
91 | await waitJobs(servers) | ||
92 | |||
93 | // Server 1 and server 2 follow each other | ||
94 | await doubleFollow(servers[0], servers[1]) | ||
95 | // Server 1 and server 3 follow each other | ||
96 | await doubleFollow(servers[0], servers[2]) | ||
97 | // Server 2 and server 3 follow each other | ||
98 | await doubleFollow(servers[1], servers[2]) | ||
99 | |||
100 | await waitJobs(servers) | ||
101 | } | ||
102 | |||
103 | async function ensureSameFilenames (videoUUID: string) { | ||
104 | let webVideoFilenames: string[] | ||
105 | let hlsFilenames: string[] | ||
106 | |||
107 | for (const server of servers) { | ||
108 | const video = await server.videos.getWithToken({ id: videoUUID }) | ||
109 | |||
110 | // Ensure we use the same filenames that the origin | ||
111 | |||
112 | const localWebVideoFilenames = video.files.map(f => basename(f.fileUrl)).sort() | ||
113 | const localHLSFilenames = video.streamingPlaylists[0].files.map(f => basename(f.fileUrl)).sort() | ||
114 | |||
115 | if (webVideoFilenames) expect(webVideoFilenames).to.deep.equal(localWebVideoFilenames) | ||
116 | else webVideoFilenames = localWebVideoFilenames | ||
117 | |||
118 | if (hlsFilenames) expect(hlsFilenames).to.deep.equal(localHLSFilenames) | ||
119 | else hlsFilenames = localHLSFilenames | ||
120 | } | ||
121 | |||
122 | return { webVideoFilenames, hlsFilenames } | ||
123 | } | ||
124 | |||
125 | async function check1WebSeed (videoUUID?: string) { | ||
126 | if (!videoUUID) videoUUID = video1Server2.uuid | ||
127 | |||
128 | const webseeds = [ | ||
129 | `${servers[1].url}/static/web-videos/` | ||
130 | ] | ||
131 | |||
132 | for (const server of servers) { | ||
133 | // With token to avoid issues with video follow constraints | ||
134 | const video = await server.videos.getWithToken({ id: videoUUID }) | ||
135 | |||
136 | for (const f of video.files) { | ||
137 | await checkMagnetWebseeds(f, webseeds, server) | ||
138 | } | ||
139 | } | ||
140 | |||
141 | await ensureSameFilenames(videoUUID) | ||
142 | } | ||
143 | |||
144 | async function check2Webseeds (videoUUID?: string) { | ||
145 | if (!videoUUID) videoUUID = video1Server2.uuid | ||
146 | |||
147 | const webseeds = [ | ||
148 | `${servers[0].url}/static/redundancy/`, | ||
149 | `${servers[1].url}/static/web-videos/` | ||
150 | ] | ||
151 | |||
152 | for (const server of servers) { | ||
153 | const video = await server.videos.get({ id: videoUUID }) | ||
154 | |||
155 | for (const file of video.files) { | ||
156 | await checkMagnetWebseeds(file, webseeds, server) | ||
157 | } | ||
158 | } | ||
159 | |||
160 | const { webVideoFilenames } = await ensureSameFilenames(videoUUID) | ||
161 | |||
162 | const directories = [ | ||
163 | servers[0].getDirectoryPath('redundancy'), | ||
164 | servers[1].getDirectoryPath('web-videos') | ||
165 | ] | ||
166 | |||
167 | for (const directory of directories) { | ||
168 | const files = await readdir(directory) | ||
169 | expect(files).to.have.length.at.least(4) | ||
170 | |||
171 | // Ensure we files exist on disk | ||
172 | expect(files.find(f => webVideoFilenames.includes(f))).to.exist | ||
173 | } | ||
174 | } | ||
175 | |||
176 | async function check0PlaylistRedundancies (videoUUID?: string) { | ||
177 | if (!videoUUID) videoUUID = video1Server2.uuid | ||
178 | |||
179 | for (const server of servers) { | ||
180 | // With token to avoid issues with video follow constraints | ||
181 | const video = await server.videos.getWithToken({ id: videoUUID }) | ||
182 | |||
183 | expect(video.streamingPlaylists).to.be.an('array') | ||
184 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
185 | expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0) | ||
186 | } | ||
187 | |||
188 | await ensureSameFilenames(videoUUID) | ||
189 | } | ||
190 | |||
191 | async function check1PlaylistRedundancies (videoUUID?: string) { | ||
192 | if (!videoUUID) videoUUID = video1Server2.uuid | ||
193 | |||
194 | for (const server of servers) { | ||
195 | const video = await server.videos.get({ id: videoUUID }) | ||
196 | |||
197 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
198 | expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1) | ||
199 | |||
200 | const redundancy = video.streamingPlaylists[0].redundancies[0] | ||
201 | |||
202 | expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID) | ||
203 | } | ||
204 | |||
205 | const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls/' + videoUUID | ||
206 | const baseUrlSegment = servers[0].url + '/static/redundancy/hls/' + videoUUID | ||
207 | |||
208 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
209 | const hlsPlaylist = video.streamingPlaylists[0] | ||
210 | |||
211 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
212 | await checkSegmentHash({ server: servers[1], baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist }) | ||
213 | } | ||
214 | |||
215 | const { hlsFilenames } = await ensureSameFilenames(videoUUID) | ||
216 | |||
217 | const directories = [ | ||
218 | servers[0].getDirectoryPath('redundancy/hls'), | ||
219 | servers[1].getDirectoryPath('streaming-playlists/hls') | ||
220 | ] | ||
221 | |||
222 | for (const directory of directories) { | ||
223 | const files = await readdir(join(directory, videoUUID)) | ||
224 | expect(files).to.have.length.at.least(4) | ||
225 | |||
226 | // Ensure we files exist on disk | ||
227 | expect(files.find(f => hlsFilenames.includes(f))).to.exist | ||
228 | } | ||
229 | } | ||
230 | |||
231 | async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) { | ||
232 | let totalSize: number = null | ||
233 | let statsLength = 1 | ||
234 | |||
235 | if (strategy !== 'manual') { | ||
236 | totalSize = 409600 | ||
237 | statsLength = 2 | ||
238 | } | ||
239 | |||
240 | const data = await servers[0].stats.get() | ||
241 | expect(data.videosRedundancy).to.have.lengthOf(statsLength) | ||
242 | |||
243 | const stat = data.videosRedundancy[0] | ||
244 | expect(stat.strategy).to.equal(strategy) | ||
245 | expect(stat.totalSize).to.equal(totalSize) | ||
246 | |||
247 | return stat | ||
248 | } | ||
249 | |||
250 | async function checkStatsWith1Redundancy (strategy: VideoRedundancyStrategyWithManual, onlyHls = false) { | ||
251 | const stat = await checkStatsGlobal(strategy) | ||
252 | |||
253 | expect(stat.totalUsed).to.be.at.least(1).and.below(409601) | ||
254 | expect(stat.totalVideoFiles).to.equal(onlyHls ? 4 : 8) | ||
255 | expect(stat.totalVideos).to.equal(1) | ||
256 | } | ||
257 | |||
258 | async function checkStatsWithoutRedundancy (strategy: VideoRedundancyStrategyWithManual) { | ||
259 | const stat = await checkStatsGlobal(strategy) | ||
260 | |||
261 | expect(stat.totalUsed).to.equal(0) | ||
262 | expect(stat.totalVideoFiles).to.equal(0) | ||
263 | expect(stat.totalVideos).to.equal(0) | ||
264 | } | ||
265 | |||
266 | async function findServerFollows () { | ||
267 | const body = await servers[0].follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' }) | ||
268 | const follows = body.data | ||
269 | const server2 = follows.find(f => f.following.host === `${servers[1].host}`) | ||
270 | const server3 = follows.find(f => f.following.host === `${servers[2].host}`) | ||
271 | |||
272 | return { server2, server3 } | ||
273 | } | ||
274 | |||
275 | async function enableRedundancyOnServer1 () { | ||
276 | await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: true }) | ||
277 | |||
278 | const { server2, server3 } = await findServerFollows() | ||
279 | |||
280 | expect(server3).to.not.be.undefined | ||
281 | expect(server3.following.hostRedundancyAllowed).to.be.false | ||
282 | |||
283 | expect(server2).to.not.be.undefined | ||
284 | expect(server2.following.hostRedundancyAllowed).to.be.true | ||
285 | } | ||
286 | |||
287 | async function disableRedundancyOnServer1 () { | ||
288 | await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: false }) | ||
289 | |||
290 | const { server2, server3 } = await findServerFollows() | ||
291 | |||
292 | expect(server3).to.not.be.undefined | ||
293 | expect(server3.following.hostRedundancyAllowed).to.be.false | ||
294 | |||
295 | expect(server2).to.not.be.undefined | ||
296 | expect(server2.following.hostRedundancyAllowed).to.be.false | ||
297 | } | ||
298 | |||
299 | describe('Test videos redundancy', function () { | ||
300 | |||
301 | describe('With most-views strategy', function () { | ||
302 | const strategy = 'most-views' | ||
303 | |||
304 | before(function () { | ||
305 | this.timeout(240000) | ||
306 | |||
307 | return createServers(strategy) | ||
308 | }) | ||
309 | |||
310 | it('Should have 1 webseed on the first video', async function () { | ||
311 | await check1WebSeed() | ||
312 | await check0PlaylistRedundancies() | ||
313 | await checkStatsWithoutRedundancy(strategy) | ||
314 | }) | ||
315 | |||
316 | it('Should enable redundancy on server 1', function () { | ||
317 | return enableRedundancyOnServer1() | ||
318 | }) | ||
319 | |||
320 | it('Should have 2 webseeds on the first video', async function () { | ||
321 | this.timeout(80000) | ||
322 | |||
323 | await waitJobs(servers) | ||
324 | await servers[0].servers.waitUntilLog('Duplicated ', 5) | ||
325 | await waitJobs(servers) | ||
326 | |||
327 | await check2Webseeds() | ||
328 | await check1PlaylistRedundancies() | ||
329 | await checkStatsWith1Redundancy(strategy) | ||
330 | }) | ||
331 | |||
332 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { | ||
333 | this.timeout(80000) | ||
334 | |||
335 | await disableRedundancyOnServer1() | ||
336 | |||
337 | await waitJobs(servers) | ||
338 | await wait(5000) | ||
339 | |||
340 | await check1WebSeed() | ||
341 | await check0PlaylistRedundancies() | ||
342 | |||
343 | await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true }) | ||
344 | }) | ||
345 | |||
346 | after(async function () { | ||
347 | return cleanupTests(servers) | ||
348 | }) | ||
349 | }) | ||
350 | |||
351 | describe('With trending strategy', function () { | ||
352 | const strategy = 'trending' | ||
353 | |||
354 | before(function () { | ||
355 | this.timeout(240000) | ||
356 | |||
357 | return createServers(strategy) | ||
358 | }) | ||
359 | |||
360 | it('Should have 1 webseed on the first video', async function () { | ||
361 | await check1WebSeed() | ||
362 | await check0PlaylistRedundancies() | ||
363 | await checkStatsWithoutRedundancy(strategy) | ||
364 | }) | ||
365 | |||
366 | it('Should enable redundancy on server 1', function () { | ||
367 | return enableRedundancyOnServer1() | ||
368 | }) | ||
369 | |||
370 | it('Should have 2 webseeds on the first video', async function () { | ||
371 | this.timeout(80000) | ||
372 | |||
373 | await waitJobs(servers) | ||
374 | await servers[0].servers.waitUntilLog('Duplicated ', 5) | ||
375 | await waitJobs(servers) | ||
376 | |||
377 | await check2Webseeds() | ||
378 | await check1PlaylistRedundancies() | ||
379 | await checkStatsWith1Redundancy(strategy) | ||
380 | }) | ||
381 | |||
382 | it('Should unfollow server 3 and keep duplicated videos', async function () { | ||
383 | this.timeout(80000) | ||
384 | |||
385 | await servers[0].follows.unfollow({ target: servers[2] }) | ||
386 | |||
387 | await waitJobs(servers) | ||
388 | await wait(5000) | ||
389 | |||
390 | await check2Webseeds() | ||
391 | await check1PlaylistRedundancies() | ||
392 | await checkStatsWith1Redundancy(strategy) | ||
393 | }) | ||
394 | |||
395 | it('Should unfollow server 2 and remove duplicated videos', async function () { | ||
396 | this.timeout(80000) | ||
397 | |||
398 | await servers[0].follows.unfollow({ target: servers[1] }) | ||
399 | |||
400 | await waitJobs(servers) | ||
401 | await wait(5000) | ||
402 | |||
403 | await check1WebSeed() | ||
404 | await check0PlaylistRedundancies() | ||
405 | |||
406 | await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true }) | ||
407 | }) | ||
408 | |||
409 | after(async function () { | ||
410 | await cleanupTests(servers) | ||
411 | }) | ||
412 | }) | ||
413 | |||
414 | describe('With recently added strategy', function () { | ||
415 | const strategy = 'recently-added' | ||
416 | |||
417 | before(function () { | ||
418 | this.timeout(240000) | ||
419 | |||
420 | return createServers(strategy, { min_views: 3 }) | ||
421 | }) | ||
422 | |||
423 | it('Should have 1 webseed on the first video', async function () { | ||
424 | await check1WebSeed() | ||
425 | await check0PlaylistRedundancies() | ||
426 | await checkStatsWithoutRedundancy(strategy) | ||
427 | }) | ||
428 | |||
429 | it('Should enable redundancy on server 1', function () { | ||
430 | return enableRedundancyOnServer1() | ||
431 | }) | ||
432 | |||
433 | it('Should still have 1 webseed on the first video', async function () { | ||
434 | this.timeout(80000) | ||
435 | |||
436 | await waitJobs(servers) | ||
437 | await wait(15000) | ||
438 | await waitJobs(servers) | ||
439 | |||
440 | await check1WebSeed() | ||
441 | await check0PlaylistRedundancies() | ||
442 | await checkStatsWithoutRedundancy(strategy) | ||
443 | }) | ||
444 | |||
445 | it('Should view 2 times the first video to have > min_views config', async function () { | ||
446 | this.timeout(80000) | ||
447 | |||
448 | await servers[0].views.simulateView({ id: video1Server2.uuid }) | ||
449 | await servers[2].views.simulateView({ id: video1Server2.uuid }) | ||
450 | |||
451 | await wait(10000) | ||
452 | await waitJobs(servers) | ||
453 | }) | ||
454 | |||
455 | it('Should have 2 webseeds on the first video', async function () { | ||
456 | this.timeout(80000) | ||
457 | |||
458 | await waitJobs(servers) | ||
459 | await servers[0].servers.waitUntilLog('Duplicated ', 5) | ||
460 | await waitJobs(servers) | ||
461 | |||
462 | await check2Webseeds() | ||
463 | await check1PlaylistRedundancies() | ||
464 | await checkStatsWith1Redundancy(strategy) | ||
465 | }) | ||
466 | |||
467 | it('Should remove the video and the redundancy files', async function () { | ||
468 | this.timeout(20000) | ||
469 | |||
470 | await saveVideoInServers(servers, video1Server2.uuid) | ||
471 | await servers[1].videos.remove({ id: video1Server2.uuid }) | ||
472 | |||
473 | await waitJobs(servers) | ||
474 | |||
475 | for (const server of servers) { | ||
476 | await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails }) | ||
477 | } | ||
478 | }) | ||
479 | |||
480 | after(async function () { | ||
481 | await cleanupTests(servers) | ||
482 | }) | ||
483 | }) | ||
484 | |||
485 | describe('With only HLS files', function () { | ||
486 | const strategy = 'recently-added' | ||
487 | |||
488 | before(async function () { | ||
489 | this.timeout(240000) | ||
490 | |||
491 | await createServers(strategy, { min_views: 3 }, false) | ||
492 | }) | ||
493 | |||
494 | it('Should have 0 playlist redundancy on the first video', async function () { | ||
495 | await check1WebSeed() | ||
496 | await check0PlaylistRedundancies() | ||
497 | }) | ||
498 | |||
499 | it('Should enable redundancy on server 1', function () { | ||
500 | return enableRedundancyOnServer1() | ||
501 | }) | ||
502 | |||
503 | it('Should still have 0 redundancy on the first video', async function () { | ||
504 | this.timeout(80000) | ||
505 | |||
506 | await waitJobs(servers) | ||
507 | await wait(15000) | ||
508 | await waitJobs(servers) | ||
509 | |||
510 | await check0PlaylistRedundancies() | ||
511 | await checkStatsWithoutRedundancy(strategy) | ||
512 | }) | ||
513 | |||
514 | it('Should have 1 redundancy on the first video', async function () { | ||
515 | this.timeout(160000) | ||
516 | |||
517 | await servers[0].views.simulateView({ id: video1Server2.uuid }) | ||
518 | await servers[2].views.simulateView({ id: video1Server2.uuid }) | ||
519 | |||
520 | await wait(10000) | ||
521 | await waitJobs(servers) | ||
522 | |||
523 | await waitJobs(servers) | ||
524 | await servers[0].servers.waitUntilLog('Duplicated ', 1) | ||
525 | await waitJobs(servers) | ||
526 | |||
527 | await check1PlaylistRedundancies() | ||
528 | await checkStatsWith1Redundancy(strategy, true) | ||
529 | }) | ||
530 | |||
531 | it('Should remove the video and the redundancy files', async function () { | ||
532 | this.timeout(20000) | ||
533 | |||
534 | await saveVideoInServers(servers, video1Server2.uuid) | ||
535 | await servers[1].videos.remove({ id: video1Server2.uuid }) | ||
536 | |||
537 | await waitJobs(servers) | ||
538 | |||
539 | for (const server of servers) { | ||
540 | await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails }) | ||
541 | } | ||
542 | }) | ||
543 | |||
544 | after(async function () { | ||
545 | await cleanupTests(servers) | ||
546 | }) | ||
547 | }) | ||
548 | |||
549 | describe('With manual strategy', function () { | ||
550 | before(function () { | ||
551 | this.timeout(240000) | ||
552 | |||
553 | return createServers(null) | ||
554 | }) | ||
555 | |||
556 | it('Should have 1 webseed on the first video', async function () { | ||
557 | await check1WebSeed() | ||
558 | await check0PlaylistRedundancies() | ||
559 | await checkStatsWithoutRedundancy('manual') | ||
560 | }) | ||
561 | |||
562 | it('Should create a redundancy on first video', async function () { | ||
563 | await servers[0].redundancy.addVideo({ videoId: video1Server2.id }) | ||
564 | }) | ||
565 | |||
566 | it('Should have 2 webseeds on the first video', async function () { | ||
567 | this.timeout(80000) | ||
568 | |||
569 | await waitJobs(servers) | ||
570 | await servers[0].servers.waitUntilLog('Duplicated ', 5) | ||
571 | await waitJobs(servers) | ||
572 | |||
573 | await check2Webseeds() | ||
574 | await check1PlaylistRedundancies() | ||
575 | await checkStatsWith1Redundancy('manual') | ||
576 | }) | ||
577 | |||
578 | it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () { | ||
579 | this.timeout(80000) | ||
580 | |||
581 | const body = await servers[0].redundancy.listVideos({ target: 'remote-videos' }) | ||
582 | |||
583 | const videos = body.data | ||
584 | expect(videos).to.have.lengthOf(1) | ||
585 | |||
586 | const video = videos[0] | ||
587 | |||
588 | for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) { | ||
589 | await servers[0].redundancy.removeVideo({ redundancyId: r.id }) | ||
590 | } | ||
591 | |||
592 | await waitJobs(servers) | ||
593 | await wait(5000) | ||
594 | |||
595 | await check1WebSeed() | ||
596 | await check0PlaylistRedundancies() | ||
597 | |||
598 | await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true }) | ||
599 | }) | ||
600 | |||
601 | after(async function () { | ||
602 | await cleanupTests(servers) | ||
603 | }) | ||
604 | }) | ||
605 | |||
606 | describe('Test expiration', function () { | ||
607 | const strategy = 'recently-added' | ||
608 | |||
609 | async function checkContains (servers: PeerTubeServer[], str: string) { | ||
610 | for (const server of servers) { | ||
611 | const video = await server.videos.get({ id: video1Server2.uuid }) | ||
612 | |||
613 | for (const f of video.files) { | ||
614 | expect(f.magnetUri).to.contain(str) | ||
615 | } | ||
616 | } | ||
617 | } | ||
618 | |||
619 | async function checkNotContains (servers: PeerTubeServer[], str: string) { | ||
620 | for (const server of servers) { | ||
621 | const video = await server.videos.get({ id: video1Server2.uuid }) | ||
622 | |||
623 | for (const f of video.files) { | ||
624 | expect(f.magnetUri).to.not.contain(str) | ||
625 | } | ||
626 | } | ||
627 | } | ||
628 | |||
629 | before(async function () { | ||
630 | this.timeout(240000) | ||
631 | |||
632 | await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) | ||
633 | |||
634 | await enableRedundancyOnServer1() | ||
635 | }) | ||
636 | |||
637 | it('Should still have 2 webseeds after 10 seconds', async function () { | ||
638 | this.timeout(80000) | ||
639 | |||
640 | await wait(10000) | ||
641 | |||
642 | try { | ||
643 | await checkContains(servers, 'http%3A%2F%2F' + servers[0].hostname + '%3A' + servers[0].port) | ||
644 | } catch { | ||
645 | // Maybe a server deleted a redundancy in the scheduler | ||
646 | await wait(2000) | ||
647 | |||
648 | await checkContains(servers, 'http%3A%2F%2F' + servers[0].hostname + '%3A' + servers[0].port) | ||
649 | } | ||
650 | }) | ||
651 | |||
652 | it('Should stop server 1 and expire video redundancy', async function () { | ||
653 | this.timeout(80000) | ||
654 | |||
655 | await killallServers([ servers[0] ]) | ||
656 | |||
657 | await wait(15000) | ||
658 | |||
659 | await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2F' + servers[0].port + '%3A' + servers[0].port) | ||
660 | }) | ||
661 | |||
662 | after(async function () { | ||
663 | await cleanupTests(servers) | ||
664 | }) | ||
665 | }) | ||
666 | |||
667 | describe('Test file replacement', function () { | ||
668 | let video2Server2UUID: string | ||
669 | const strategy = 'recently-added' | ||
670 | |||
671 | before(async function () { | ||
672 | this.timeout(240000) | ||
673 | |||
674 | await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) | ||
675 | |||
676 | await enableRedundancyOnServer1() | ||
677 | |||
678 | await waitJobs(servers) | ||
679 | await servers[0].servers.waitUntilLog('Duplicated ', 5) | ||
680 | await waitJobs(servers) | ||
681 | |||
682 | await check2Webseeds() | ||
683 | await check1PlaylistRedundancies() | ||
684 | await checkStatsWith1Redundancy(strategy) | ||
685 | |||
686 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2', privacy: VideoPrivacy.PRIVATE } }) | ||
687 | video2Server2UUID = uuid | ||
688 | |||
689 | // Wait transcoding before federation | ||
690 | await waitJobs(servers) | ||
691 | |||
692 | await servers[1].videos.update({ id: video2Server2UUID, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
693 | }) | ||
694 | |||
695 | it('Should cache video 2 webseeds on the first video', async function () { | ||
696 | this.timeout(240000) | ||
697 | |||
698 | await waitJobs(servers) | ||
699 | |||
700 | let checked = false | ||
701 | |||
702 | while (checked === false) { | ||
703 | await wait(1000) | ||
704 | |||
705 | try { | ||
706 | await check1WebSeed() | ||
707 | await check0PlaylistRedundancies() | ||
708 | |||
709 | await check2Webseeds(video2Server2UUID) | ||
710 | await check1PlaylistRedundancies(video2Server2UUID) | ||
711 | |||
712 | checked = true | ||
713 | } catch { | ||
714 | checked = false | ||
715 | } | ||
716 | } | ||
717 | }) | ||
718 | |||
719 | it('Should disable strategy and remove redundancies', async function () { | ||
720 | this.timeout(80000) | ||
721 | |||
722 | await waitJobs(servers) | ||
723 | |||
724 | await killallServers([ servers[0] ]) | ||
725 | await servers[0].run({ | ||
726 | redundancy: { | ||
727 | videos: { | ||
728 | check_interval: '1 second', | ||
729 | strategies: [] | ||
730 | } | ||
731 | } | ||
732 | }) | ||
733 | |||
734 | await waitJobs(servers) | ||
735 | |||
736 | await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true }) | ||
737 | }) | ||
738 | |||
739 | after(async function () { | ||
740 | await cleanupTests(servers) | ||
741 | }) | ||
742 | }) | ||
743 | }) | ||
diff --git a/packages/tests/src/api/runners/index.ts b/packages/tests/src/api/runners/index.ts new file mode 100644 index 000000000..441ddc874 --- /dev/null +++ b/packages/tests/src/api/runners/index.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | export * from './runner-common.js' | ||
2 | export * from './runner-live-transcoding.js' | ||
3 | export * from './runner-socket.js' | ||
4 | export * from './runner-studio-transcoding.js' | ||
5 | export * from './runner-vod-transcoding.js' | ||
diff --git a/packages/tests/src/api/runners/runner-common.ts b/packages/tests/src/api/runners/runner-common.ts new file mode 100644 index 000000000..53ea321d0 --- /dev/null +++ b/packages/tests/src/api/runners/runner-common.ts | |||
@@ -0,0 +1,744 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { | ||
5 | HttpStatusCode, | ||
6 | Runner, | ||
7 | RunnerJob, | ||
8 | RunnerJobAdmin, | ||
9 | RunnerJobState, | ||
10 | RunnerJobStateType, | ||
11 | RunnerJobVODWebVideoTranscodingPayload, | ||
12 | RunnerRegistrationToken | ||
13 | } from '@peertube/peertube-models' | ||
14 | import { | ||
15 | PeerTubeServer, | ||
16 | cleanupTests, | ||
17 | createSingleServer, | ||
18 | setAccessTokensToServers, | ||
19 | setDefaultVideoChannel, | ||
20 | waitJobs | ||
21 | } from '@peertube/peertube-server-commands' | ||
22 | import { expect } from 'chai' | ||
23 | |||
24 | describe('Test runner common actions', function () { | ||
25 | let server: PeerTubeServer | ||
26 | let registrationToken: string | ||
27 | let runnerToken: string | ||
28 | let jobMaxPriority: string | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120_000) | ||
32 | |||
33 | server = await createSingleServer(1, { | ||
34 | remote_runners: { | ||
35 | stalled_jobs: { | ||
36 | vod: '5 seconds' | ||
37 | } | ||
38 | } | ||
39 | }) | ||
40 | |||
41 | await setAccessTokensToServers([ server ]) | ||
42 | await setDefaultVideoChannel([ server ]) | ||
43 | |||
44 | await server.config.enableTranscoding({ hls: true, webVideo: true }) | ||
45 | await server.config.enableRemoteTranscoding() | ||
46 | }) | ||
47 | |||
48 | describe('Managing runner registration tokens', function () { | ||
49 | let base: RunnerRegistrationToken[] | ||
50 | let registrationTokenToDelete: RunnerRegistrationToken | ||
51 | |||
52 | it('Should have a default registration token', async function () { | ||
53 | const { total, data } = await server.runnerRegistrationTokens.list() | ||
54 | |||
55 | expect(total).to.equal(1) | ||
56 | expect(data).to.have.lengthOf(1) | ||
57 | |||
58 | const token = data[0] | ||
59 | expect(token.id).to.exist | ||
60 | expect(token.createdAt).to.exist | ||
61 | expect(token.updatedAt).to.exist | ||
62 | expect(token.registeredRunnersCount).to.equal(0) | ||
63 | expect(token.registrationToken).to.exist | ||
64 | }) | ||
65 | |||
66 | it('Should create other registration tokens', async function () { | ||
67 | await server.runnerRegistrationTokens.generate() | ||
68 | await server.runnerRegistrationTokens.generate() | ||
69 | |||
70 | const { total, data } = await server.runnerRegistrationTokens.list() | ||
71 | expect(total).to.equal(3) | ||
72 | expect(data).to.have.lengthOf(3) | ||
73 | }) | ||
74 | |||
75 | it('Should list registration tokens', async function () { | ||
76 | { | ||
77 | const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' }) | ||
78 | expect(total).to.equal(3) | ||
79 | expect(data).to.have.lengthOf(3) | ||
80 | expect(new Date(data[0].createdAt)).to.be.below(new Date(data[1].createdAt)) | ||
81 | expect(new Date(data[1].createdAt)).to.be.below(new Date(data[2].createdAt)) | ||
82 | |||
83 | base = data | ||
84 | |||
85 | registrationTokenToDelete = data[0] | ||
86 | registrationToken = data[1].registrationToken | ||
87 | } | ||
88 | |||
89 | { | ||
90 | const { total, data } = await server.runnerRegistrationTokens.list({ sort: '-createdAt', start: 2, count: 1 }) | ||
91 | expect(total).to.equal(3) | ||
92 | expect(data).to.have.lengthOf(1) | ||
93 | expect(data[0].registrationToken).to.equal(base[0].registrationToken) | ||
94 | } | ||
95 | }) | ||
96 | |||
97 | it('Should have appropriate registeredRunnersCount for registration tokens', async function () { | ||
98 | await server.runners.register({ name: 'to delete 1', registrationToken: registrationTokenToDelete.registrationToken }) | ||
99 | await server.runners.register({ name: 'to delete 2', registrationToken: registrationTokenToDelete.registrationToken }) | ||
100 | |||
101 | const { data } = await server.runnerRegistrationTokens.list() | ||
102 | |||
103 | for (const d of data) { | ||
104 | if (d.registrationToken === registrationTokenToDelete.registrationToken) { | ||
105 | expect(d.registeredRunnersCount).to.equal(2) | ||
106 | } else { | ||
107 | expect(d.registeredRunnersCount).to.equal(0) | ||
108 | } | ||
109 | } | ||
110 | |||
111 | const { data: runners } = await server.runners.list() | ||
112 | expect(runners).to.have.lengthOf(2) | ||
113 | }) | ||
114 | |||
115 | it('Should delete a registration token', async function () { | ||
116 | await server.runnerRegistrationTokens.delete({ id: registrationTokenToDelete.id }) | ||
117 | |||
118 | const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' }) | ||
119 | expect(total).to.equal(2) | ||
120 | expect(data).to.have.lengthOf(2) | ||
121 | |||
122 | for (const d of data) { | ||
123 | expect(d.registeredRunnersCount).to.equal(0) | ||
124 | expect(d.registrationToken).to.not.equal(registrationTokenToDelete.registrationToken) | ||
125 | } | ||
126 | }) | ||
127 | |||
128 | it('Should have removed runners of this registration token', async function () { | ||
129 | const { data: runners } = await server.runners.list() | ||
130 | expect(runners).to.have.lengthOf(0) | ||
131 | }) | ||
132 | }) | ||
133 | |||
134 | describe('Managing runners', function () { | ||
135 | let toDelete: Runner | ||
136 | |||
137 | it('Should not have runners available', async function () { | ||
138 | const { total, data } = await server.runners.list() | ||
139 | |||
140 | expect(data).to.have.lengthOf(0) | ||
141 | expect(total).to.equal(0) | ||
142 | }) | ||
143 | |||
144 | it('Should register runners', async function () { | ||
145 | const now = new Date() | ||
146 | |||
147 | const result = await server.runners.register({ | ||
148 | name: 'runner 1', | ||
149 | description: 'my super runner 1', | ||
150 | registrationToken | ||
151 | }) | ||
152 | expect(result.runnerToken).to.exist | ||
153 | runnerToken = result.runnerToken | ||
154 | |||
155 | await server.runners.register({ | ||
156 | name: 'runner 2', | ||
157 | registrationToken | ||
158 | }) | ||
159 | |||
160 | const { total, data } = await server.runners.list({ sort: 'createdAt' }) | ||
161 | expect(total).to.equal(2) | ||
162 | expect(data).to.have.lengthOf(2) | ||
163 | |||
164 | for (const d of data) { | ||
165 | expect(d.id).to.exist | ||
166 | expect(d.createdAt).to.exist | ||
167 | expect(d.updatedAt).to.exist | ||
168 | expect(new Date(d.createdAt)).to.be.above(now) | ||
169 | expect(new Date(d.updatedAt)).to.be.above(now) | ||
170 | expect(new Date(d.lastContact)).to.be.above(now) | ||
171 | expect(d.ip).to.exist | ||
172 | } | ||
173 | |||
174 | expect(data[0].name).to.equal('runner 1') | ||
175 | expect(data[0].description).to.equal('my super runner 1') | ||
176 | |||
177 | expect(data[1].name).to.equal('runner 2') | ||
178 | expect(data[1].description).to.be.null | ||
179 | |||
180 | toDelete = data[1] | ||
181 | }) | ||
182 | |||
183 | it('Should list runners', async function () { | ||
184 | const { total, data } = await server.runners.list({ sort: '-createdAt', start: 1, count: 1 }) | ||
185 | |||
186 | expect(total).to.equal(2) | ||
187 | expect(data).to.have.lengthOf(1) | ||
188 | expect(data[0].name).to.equal('runner 1') | ||
189 | }) | ||
190 | |||
191 | it('Should delete a runner', async function () { | ||
192 | await server.runners.delete({ id: toDelete.id }) | ||
193 | |||
194 | const { total, data } = await server.runners.list() | ||
195 | |||
196 | expect(total).to.equal(1) | ||
197 | expect(data).to.have.lengthOf(1) | ||
198 | expect(data[0].name).to.equal('runner 1') | ||
199 | }) | ||
200 | |||
201 | it('Should unregister a runner', async function () { | ||
202 | const registered = await server.runners.autoRegisterRunner() | ||
203 | |||
204 | { | ||
205 | const { total, data } = await server.runners.list() | ||
206 | expect(total).to.equal(2) | ||
207 | expect(data).to.have.lengthOf(2) | ||
208 | } | ||
209 | |||
210 | await server.runners.unregister({ runnerToken: registered }) | ||
211 | |||
212 | { | ||
213 | const { total, data } = await server.runners.list() | ||
214 | expect(total).to.equal(1) | ||
215 | expect(data).to.have.lengthOf(1) | ||
216 | expect(data[0].name).to.equal('runner 1') | ||
217 | } | ||
218 | }) | ||
219 | }) | ||
220 | |||
221 | describe('Managing runner jobs', function () { | ||
222 | let jobUUID: string | ||
223 | let jobToken: string | ||
224 | let lastRunnerContact: Date | ||
225 | let failedJob: RunnerJob | ||
226 | |||
227 | async function checkMainJobState ( | ||
228 | mainJobState: RunnerJobStateType, | ||
229 | otherJobStates: RunnerJobStateType[] = [ RunnerJobState.PENDING, RunnerJobState.WAITING_FOR_PARENT_JOB ] | ||
230 | ) { | ||
231 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
232 | |||
233 | for (const job of data) { | ||
234 | if (job.uuid === jobUUID) { | ||
235 | expect(job.state.id).to.equal(mainJobState) | ||
236 | } else { | ||
237 | expect(otherJobStates).to.include(job.state.id) | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | |||
242 | function getMainJob () { | ||
243 | return server.runnerJobs.getJob({ uuid: jobUUID }) | ||
244 | } | ||
245 | |||
246 | describe('List jobs', function () { | ||
247 | |||
248 | it('Should not have jobs', async function () { | ||
249 | const { total, data } = await server.runnerJobs.list() | ||
250 | |||
251 | expect(data).to.have.lengthOf(0) | ||
252 | expect(total).to.equal(0) | ||
253 | }) | ||
254 | |||
255 | it('Should upload a video and have available jobs', async function () { | ||
256 | await server.videos.quickUpload({ name: 'to transcode' }) | ||
257 | await waitJobs([ server ]) | ||
258 | |||
259 | const { total, data } = await server.runnerJobs.list() | ||
260 | |||
261 | expect(data).to.have.lengthOf(10) | ||
262 | expect(total).to.equal(10) | ||
263 | |||
264 | for (const job of data) { | ||
265 | expect(job.startedAt).to.not.exist | ||
266 | expect(job.finishedAt).to.not.exist | ||
267 | expect(job.payload).to.exist | ||
268 | expect(job.privatePayload).to.exist | ||
269 | } | ||
270 | |||
271 | const hlsJobs = data.filter(d => d.type === 'vod-hls-transcoding') | ||
272 | const webVideoJobs = data.filter(d => d.type === 'vod-web-video-transcoding') | ||
273 | |||
274 | expect(hlsJobs).to.have.lengthOf(5) | ||
275 | expect(webVideoJobs).to.have.lengthOf(5) | ||
276 | |||
277 | const pendingJobs = data.filter(d => d.state.id === RunnerJobState.PENDING) | ||
278 | const waitingJobs = data.filter(d => d.state.id === RunnerJobState.WAITING_FOR_PARENT_JOB) | ||
279 | |||
280 | expect(pendingJobs).to.have.lengthOf(1) | ||
281 | expect(waitingJobs).to.have.lengthOf(9) | ||
282 | }) | ||
283 | |||
284 | it('Should upload another video and list/sort jobs', async function () { | ||
285 | await server.videos.quickUpload({ name: 'to transcode 2' }) | ||
286 | await waitJobs([ server ]) | ||
287 | |||
288 | { | ||
289 | const { total, data } = await server.runnerJobs.list({ start: 0, count: 30 }) | ||
290 | |||
291 | expect(data).to.have.lengthOf(20) | ||
292 | expect(total).to.equal(20) | ||
293 | |||
294 | jobUUID = data[16].uuid | ||
295 | } | ||
296 | |||
297 | { | ||
298 | const { total, data } = await server.runnerJobs.list({ start: 3, count: 1, sort: 'createdAt' }) | ||
299 | expect(total).to.equal(20) | ||
300 | |||
301 | expect(data).to.have.lengthOf(1) | ||
302 | expect(data[0].uuid).to.equal(jobUUID) | ||
303 | } | ||
304 | |||
305 | { | ||
306 | let previousPriority = Infinity | ||
307 | const { total, data } = await server.runnerJobs.list({ start: 0, count: 100, sort: '-priority' }) | ||
308 | expect(total).to.equal(20) | ||
309 | |||
310 | for (const job of data) { | ||
311 | expect(job.priority).to.be.at.most(previousPriority) | ||
312 | previousPriority = job.priority | ||
313 | |||
314 | if (job.state.id === RunnerJobState.PENDING) { | ||
315 | jobMaxPriority = job.uuid | ||
316 | } | ||
317 | } | ||
318 | } | ||
319 | }) | ||
320 | |||
321 | it('Should search jobs', async function () { | ||
322 | { | ||
323 | const { total, data } = await server.runnerJobs.list({ search: jobUUID }) | ||
324 | |||
325 | expect(data).to.have.lengthOf(1) | ||
326 | expect(total).to.equal(1) | ||
327 | |||
328 | expect(data[0].uuid).to.equal(jobUUID) | ||
329 | } | ||
330 | |||
331 | { | ||
332 | const { total, data } = await server.runnerJobs.list({ search: 'toto' }) | ||
333 | |||
334 | expect(data).to.have.lengthOf(0) | ||
335 | expect(total).to.equal(0) | ||
336 | } | ||
337 | |||
338 | { | ||
339 | const { total, data } = await server.runnerJobs.list({ search: 'hls' }) | ||
340 | |||
341 | expect(data).to.not.have.lengthOf(0) | ||
342 | expect(total).to.not.equal(0) | ||
343 | |||
344 | for (const job of data) { | ||
345 | expect(job.type).to.include('hls') | ||
346 | } | ||
347 | } | ||
348 | }) | ||
349 | |||
350 | it('Should filter jobs', async function () { | ||
351 | { | ||
352 | const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.WAITING_FOR_PARENT_JOB ] }) | ||
353 | |||
354 | expect(data).to.not.have.lengthOf(0) | ||
355 | expect(total).to.not.equal(0) | ||
356 | |||
357 | for (const job of data) { | ||
358 | expect(job.state.label).to.equal('Waiting for parent job to finish') | ||
359 | } | ||
360 | } | ||
361 | |||
362 | { | ||
363 | const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.COMPLETED ] }) | ||
364 | |||
365 | expect(data).to.have.lengthOf(0) | ||
366 | expect(total).to.equal(0) | ||
367 | } | ||
368 | }) | ||
369 | }) | ||
370 | |||
371 | describe('Accept/update/abort/process a job', function () { | ||
372 | |||
373 | it('Should request available jobs', async function () { | ||
374 | lastRunnerContact = new Date() | ||
375 | |||
376 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
377 | |||
378 | // Only optimize jobs are available | ||
379 | expect(availableJobs).to.have.lengthOf(2) | ||
380 | |||
381 | for (const job of availableJobs) { | ||
382 | expect(job.uuid).to.exist | ||
383 | expect(job.payload.input).to.exist | ||
384 | expect((job.payload as RunnerJobVODWebVideoTranscodingPayload).output).to.exist | ||
385 | |||
386 | expect((job as RunnerJobAdmin).privatePayload).to.not.exist | ||
387 | } | ||
388 | |||
389 | const hlsJobs = availableJobs.filter(d => d.type === 'vod-hls-transcoding') | ||
390 | const webVideoJobs = availableJobs.filter(d => d.type === 'vod-web-video-transcoding') | ||
391 | |||
392 | expect(hlsJobs).to.have.lengthOf(0) | ||
393 | expect(webVideoJobs).to.have.lengthOf(2) | ||
394 | |||
395 | jobUUID = webVideoJobs[0].uuid | ||
396 | }) | ||
397 | |||
398 | it('Should have sorted available jobs by priority', async function () { | ||
399 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
400 | |||
401 | expect(availableJobs[0].uuid).to.equal(jobMaxPriority) | ||
402 | }) | ||
403 | |||
404 | it('Should have last runner contact updated', async function () { | ||
405 | await wait(1000) | ||
406 | |||
407 | const { data } = await server.runners.list({ sort: 'createdAt' }) | ||
408 | expect(new Date(data[0].lastContact)).to.be.above(lastRunnerContact) | ||
409 | }) | ||
410 | |||
411 | it('Should accept a job', async function () { | ||
412 | const startedAt = new Date() | ||
413 | |||
414 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
415 | jobToken = job.jobToken | ||
416 | |||
417 | const checkProcessingJob = (job: RunnerJob & { jobToken?: string }, fromAccept: boolean) => { | ||
418 | expect(job.uuid).to.equal(jobUUID) | ||
419 | |||
420 | expect(job.type).to.equal('vod-web-video-transcoding') | ||
421 | expect(job.state.label).to.equal('Processing') | ||
422 | expect(job.state.id).to.equal(RunnerJobState.PROCESSING) | ||
423 | |||
424 | expect(job.runner).to.exist | ||
425 | expect(job.runner.name).to.equal('runner 1') | ||
426 | expect(job.runner.description).to.equal('my super runner 1') | ||
427 | |||
428 | expect(job.progress).to.be.null | ||
429 | |||
430 | expect(job.startedAt).to.exist | ||
431 | expect(new Date(job.startedAt)).to.be.above(startedAt) | ||
432 | |||
433 | expect(job.finishedAt).to.not.exist | ||
434 | |||
435 | expect(job.failures).to.equal(0) | ||
436 | |||
437 | expect(job.payload).to.exist | ||
438 | |||
439 | if (fromAccept) { | ||
440 | expect(job.jobToken).to.exist | ||
441 | expect((job as RunnerJobAdmin).privatePayload).to.not.exist | ||
442 | } else { | ||
443 | expect(job.jobToken).to.not.exist | ||
444 | expect((job as RunnerJobAdmin).privatePayload).to.exist | ||
445 | } | ||
446 | } | ||
447 | |||
448 | checkProcessingJob(job, true) | ||
449 | |||
450 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
451 | |||
452 | const processingJob = data.find(j => j.uuid === jobUUID) | ||
453 | checkProcessingJob(processingJob, false) | ||
454 | |||
455 | await checkMainJobState(RunnerJobState.PROCESSING) | ||
456 | }) | ||
457 | |||
458 | it('Should update a job', async function () { | ||
459 | await server.runnerJobs.update({ runnerToken, jobUUID, jobToken, progress: 53 }) | ||
460 | |||
461 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
462 | |||
463 | for (const job of data) { | ||
464 | if (job.state.id === RunnerJobState.PROCESSING) { | ||
465 | expect(job.progress).to.equal(53) | ||
466 | } else { | ||
467 | expect(job.progress).to.be.null | ||
468 | } | ||
469 | } | ||
470 | }) | ||
471 | |||
472 | it('Should abort a job', async function () { | ||
473 | await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'for tests' }) | ||
474 | |||
475 | await checkMainJobState(RunnerJobState.PENDING) | ||
476 | |||
477 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
478 | for (const job of data) { | ||
479 | expect(job.progress).to.be.null | ||
480 | } | ||
481 | }) | ||
482 | |||
483 | it('Should accept the same job again and post a success', async function () { | ||
484 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
485 | expect(availableJobs.find(j => j.uuid === jobUUID)).to.exist | ||
486 | |||
487 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
488 | jobToken = job.jobToken | ||
489 | |||
490 | await checkMainJobState(RunnerJobState.PROCESSING) | ||
491 | |||
492 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
493 | |||
494 | for (const job of data) { | ||
495 | expect(job.progress).to.be.null | ||
496 | } | ||
497 | |||
498 | const payload = { | ||
499 | videoFile: 'video_short.mp4' | ||
500 | } | ||
501 | |||
502 | await server.runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
503 | }) | ||
504 | |||
505 | it('Should not have available jobs anymore', async function () { | ||
506 | await checkMainJobState(RunnerJobState.COMPLETED) | ||
507 | |||
508 | const job = await getMainJob() | ||
509 | expect(job.finishedAt).to.exist | ||
510 | |||
511 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
512 | expect(availableJobs.find(j => j.uuid === jobUUID)).to.not.exist | ||
513 | }) | ||
514 | }) | ||
515 | |||
516 | describe('Error job', function () { | ||
517 | |||
518 | it('Should accept another job and post an error', async function () { | ||
519 | await server.runnerJobs.cancelAllJobs() | ||
520 | await server.videos.quickUpload({ name: 'video' }) | ||
521 | await waitJobs([ server ]) | ||
522 | |||
523 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
524 | jobUUID = availableJobs[0].uuid | ||
525 | |||
526 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
527 | jobToken = job.jobToken | ||
528 | |||
529 | await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' }) | ||
530 | }) | ||
531 | |||
532 | it('Should have job failures increased', async function () { | ||
533 | const job = await getMainJob() | ||
534 | expect(job.state.id).to.equal(RunnerJobState.PENDING) | ||
535 | expect(job.failures).to.equal(1) | ||
536 | expect(job.error).to.be.null | ||
537 | expect(job.progress).to.be.null | ||
538 | expect(job.finishedAt).to.not.exist | ||
539 | }) | ||
540 | |||
541 | it('Should error a job when job attempts is too big', async function () { | ||
542 | for (let i = 0; i < 4; i++) { | ||
543 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
544 | jobToken = job.jobToken | ||
545 | |||
546 | await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error ' + i }) | ||
547 | } | ||
548 | |||
549 | const job = await getMainJob() | ||
550 | expect(job.failures).to.equal(5) | ||
551 | expect(job.state.id).to.equal(RunnerJobState.ERRORED) | ||
552 | expect(job.state.label).to.equal('Errored') | ||
553 | expect(job.error).to.equal('Error 3') | ||
554 | expect(job.progress).to.be.null | ||
555 | expect(job.finishedAt).to.exist | ||
556 | |||
557 | failedJob = job | ||
558 | }) | ||
559 | |||
560 | it('Should have failed children jobs too', async function () { | ||
561 | const { data } = await server.runnerJobs.list({ count: 50, sort: '-updatedAt' }) | ||
562 | |||
563 | const children = data.filter(j => j.parent?.uuid === failedJob.uuid) | ||
564 | expect(children).to.have.lengthOf(9) | ||
565 | |||
566 | for (const child of children) { | ||
567 | expect(child.parent.uuid).to.equal(failedJob.uuid) | ||
568 | expect(child.parent.type).to.equal(failedJob.type) | ||
569 | expect(child.parent.state.id).to.equal(failedJob.state.id) | ||
570 | expect(child.parent.state.label).to.equal(failedJob.state.label) | ||
571 | |||
572 | expect(child.state.id).to.equal(RunnerJobState.PARENT_ERRORED) | ||
573 | expect(child.state.label).to.equal('Parent job failed') | ||
574 | } | ||
575 | }) | ||
576 | }) | ||
577 | |||
578 | describe('Cancel', function () { | ||
579 | |||
580 | it('Should cancel a pending job', async function () { | ||
581 | await server.videos.quickUpload({ name: 'video' }) | ||
582 | await waitJobs([ server ]) | ||
583 | |||
584 | { | ||
585 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
586 | |||
587 | const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING) | ||
588 | jobUUID = pendingJob.uuid | ||
589 | |||
590 | await server.runnerJobs.cancelByAdmin({ jobUUID }) | ||
591 | } | ||
592 | |||
593 | { | ||
594 | const job = await getMainJob() | ||
595 | expect(job.state.id).to.equal(RunnerJobState.CANCELLED) | ||
596 | expect(job.state.label).to.equal('Cancelled') | ||
597 | } | ||
598 | |||
599 | { | ||
600 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
601 | const children = data.filter(j => j.parent?.uuid === jobUUID) | ||
602 | expect(children).to.have.lengthOf(9) | ||
603 | |||
604 | for (const child of children) { | ||
605 | expect(child.state.id).to.equal(RunnerJobState.PARENT_CANCELLED) | ||
606 | } | ||
607 | } | ||
608 | }) | ||
609 | |||
610 | it('Should cancel an already accepted job and skip success/error', async function () { | ||
611 | await server.videos.quickUpload({ name: 'video' }) | ||
612 | await waitJobs([ server ]) | ||
613 | |||
614 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
615 | jobUUID = availableJobs[0].uuid | ||
616 | |||
617 | const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID }) | ||
618 | jobToken = job.jobToken | ||
619 | |||
620 | await server.runnerJobs.cancelByAdmin({ jobUUID }) | ||
621 | |||
622 | await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'aborted', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
623 | }) | ||
624 | }) | ||
625 | |||
626 | describe('Remove', function () { | ||
627 | |||
628 | it('Should remove a pending job', async function () { | ||
629 | await server.videos.quickUpload({ name: 'video' }) | ||
630 | await waitJobs([ server ]) | ||
631 | |||
632 | { | ||
633 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
634 | |||
635 | const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING) | ||
636 | jobUUID = pendingJob.uuid | ||
637 | |||
638 | await server.runnerJobs.deleteByAdmin({ jobUUID }) | ||
639 | } | ||
640 | |||
641 | { | ||
642 | const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' }) | ||
643 | |||
644 | const parent = data.find(j => j.uuid === jobUUID) | ||
645 | expect(parent).to.not.exist | ||
646 | |||
647 | const children = data.filter(j => j.parent?.uuid === jobUUID) | ||
648 | expect(children).to.have.lengthOf(0) | ||
649 | } | ||
650 | }) | ||
651 | }) | ||
652 | |||
653 | describe('Stalled jobs', function () { | ||
654 | |||
655 | it('Should abort stalled jobs', async function () { | ||
656 | this.timeout(60000) | ||
657 | |||
658 | await server.videos.quickUpload({ name: 'video' }) | ||
659 | await server.videos.quickUpload({ name: 'video' }) | ||
660 | await waitJobs([ server ]) | ||
661 | |||
662 | const { job: job1 } = await server.runnerJobs.autoAccept({ runnerToken }) | ||
663 | const { job: stalledJob } = await server.runnerJobs.autoAccept({ runnerToken }) | ||
664 | |||
665 | for (let i = 0; i < 6; i++) { | ||
666 | await wait(2000) | ||
667 | |||
668 | await server.runnerJobs.update({ runnerToken, jobToken: job1.jobToken, jobUUID: job1.uuid }) | ||
669 | } | ||
670 | |||
671 | const refreshedJob1 = await server.runnerJobs.getJob({ uuid: job1.uuid }) | ||
672 | const refreshedStalledJob = await server.runnerJobs.getJob({ uuid: stalledJob.uuid }) | ||
673 | |||
674 | expect(refreshedJob1.state.id).to.equal(RunnerJobState.PROCESSING) | ||
675 | expect(refreshedStalledJob.state.id).to.equal(RunnerJobState.PENDING) | ||
676 | }) | ||
677 | }) | ||
678 | |||
679 | describe('Rate limit', function () { | ||
680 | |||
681 | before(async function () { | ||
682 | this.timeout(60000) | ||
683 | |||
684 | await server.kill() | ||
685 | |||
686 | await server.run({ | ||
687 | rates_limit: { | ||
688 | api: { | ||
689 | max: 10 | ||
690 | } | ||
691 | } | ||
692 | }) | ||
693 | }) | ||
694 | |||
695 | it('Should rate limit an unknown runner, but not a registered one', async function () { | ||
696 | this.timeout(60000) | ||
697 | |||
698 | await server.videos.quickUpload({ name: 'video' }) | ||
699 | await waitJobs([ server ]) | ||
700 | |||
701 | const { job } = await server.runnerJobs.autoAccept({ runnerToken }) | ||
702 | |||
703 | for (let i = 0; i < 20; i++) { | ||
704 | try { | ||
705 | await server.runnerJobs.request({ runnerToken }) | ||
706 | await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid }) | ||
707 | } catch {} | ||
708 | } | ||
709 | |||
710 | // Invalid | ||
711 | { | ||
712 | await server.runnerJobs.request({ runnerToken: 'toto', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
713 | await server.runnerJobs.update({ | ||
714 | runnerToken: 'toto', | ||
715 | jobToken: job.jobToken, | ||
716 | jobUUID: job.uuid, | ||
717 | expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 | ||
718 | }) | ||
719 | } | ||
720 | |||
721 | // Not provided | ||
722 | { | ||
723 | await server.runnerJobs.request({ runnerToken: undefined, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
724 | await server.runnerJobs.update({ | ||
725 | runnerToken: undefined, | ||
726 | jobToken: job.jobToken, | ||
727 | jobUUID: job.uuid, | ||
728 | expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 | ||
729 | }) | ||
730 | } | ||
731 | |||
732 | // Registered | ||
733 | { | ||
734 | await server.runnerJobs.request({ runnerToken }) | ||
735 | await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid }) | ||
736 | } | ||
737 | }) | ||
738 | }) | ||
739 | }) | ||
740 | |||
741 | after(async function () { | ||
742 | await cleanupTests([ server ]) | ||
743 | }) | ||
744 | }) | ||
diff --git a/packages/tests/src/api/runners/runner-live-transcoding.ts b/packages/tests/src/api/runners/runner-live-transcoding.ts new file mode 100644 index 000000000..20c1e5c2a --- /dev/null +++ b/packages/tests/src/api/runners/runner-live-transcoding.ts | |||
@@ -0,0 +1,332 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { readFile } from 'fs/promises' | ||
6 | import { wait } from '@peertube/peertube-core-utils' | ||
7 | import { | ||
8 | HttpStatusCode, | ||
9 | LiveRTMPHLSTranscodingUpdatePayload, | ||
10 | LiveVideo, | ||
11 | LiveVideoError, | ||
12 | LiveVideoErrorType, | ||
13 | RunnerJob, | ||
14 | RunnerJobLiveRTMPHLSTranscodingPayload, | ||
15 | Video, | ||
16 | VideoPrivacy, | ||
17 | VideoState | ||
18 | } from '@peertube/peertube-models' | ||
19 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
20 | import { | ||
21 | cleanupTests, | ||
22 | createSingleServer, | ||
23 | makeRawRequest, | ||
24 | PeerTubeServer, | ||
25 | sendRTMPStream, | ||
26 | setAccessTokensToServers, | ||
27 | setDefaultVideoChannel, | ||
28 | stopFfmpeg, | ||
29 | testFfmpegStreamError, | ||
30 | waitJobs | ||
31 | } from '@peertube/peertube-server-commands' | ||
32 | |||
33 | describe('Test runner live transcoding', function () { | ||
34 | let server: PeerTubeServer | ||
35 | let runnerToken: string | ||
36 | let baseUrl: string | ||
37 | |||
38 | before(async function () { | ||
39 | this.timeout(120_000) | ||
40 | |||
41 | server = await createSingleServer(1) | ||
42 | |||
43 | await setAccessTokensToServers([ server ]) | ||
44 | await setDefaultVideoChannel([ server ]) | ||
45 | |||
46 | await server.config.enableRemoteTranscoding() | ||
47 | await server.config.enableTranscoding() | ||
48 | runnerToken = await server.runners.autoRegisterRunner() | ||
49 | |||
50 | baseUrl = server.url + '/static/streaming-playlists/hls' | ||
51 | }) | ||
52 | |||
53 | describe('Without transcoding enabled', function () { | ||
54 | |||
55 | before(async function () { | ||
56 | await server.config.enableLive({ | ||
57 | allowReplay: false, | ||
58 | resolutions: 'min', | ||
59 | transcoding: false | ||
60 | }) | ||
61 | }) | ||
62 | |||
63 | it('Should not have available jobs', async function () { | ||
64 | this.timeout(120000) | ||
65 | |||
66 | const { live, video } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) | ||
67 | |||
68 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
69 | await server.live.waitUntilPublished({ videoId: video.id }) | ||
70 | |||
71 | await waitJobs([ server ]) | ||
72 | |||
73 | const { availableJobs } = await server.runnerJobs.requestLive({ runnerToken }) | ||
74 | expect(availableJobs).to.have.lengthOf(0) | ||
75 | |||
76 | await stopFfmpeg(ffmpegCommand) | ||
77 | }) | ||
78 | }) | ||
79 | |||
80 | describe('With transcoding enabled on classic live', function () { | ||
81 | let live: LiveVideo | ||
82 | let video: Video | ||
83 | let ffmpegCommand: FfmpegCommand | ||
84 | let jobUUID: string | ||
85 | let acceptedJob: RunnerJob & { jobToken: string } | ||
86 | |||
87 | async function testPlaylistFile (fixture: string, expected: string) { | ||
88 | const text = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${fixture}` }) | ||
89 | expect(await readFile(buildAbsoluteFixturePath(expected), 'utf-8')).to.equal(text) | ||
90 | |||
91 | } | ||
92 | |||
93 | async function testTSFile (fixture: string, expected: string) { | ||
94 | const { body } = await makeRawRequest({ url: `${baseUrl}/${video.uuid}/${fixture}`, expectedStatus: HttpStatusCode.OK_200 }) | ||
95 | expect(await readFile(buildAbsoluteFixturePath(expected))).to.deep.equal(body) | ||
96 | } | ||
97 | |||
98 | before(async function () { | ||
99 | await server.config.enableLive({ | ||
100 | allowReplay: true, | ||
101 | resolutions: 'max', | ||
102 | transcoding: true | ||
103 | }) | ||
104 | }) | ||
105 | |||
106 | it('Should publish a a live and have available jobs', async function () { | ||
107 | this.timeout(120000) | ||
108 | |||
109 | const data = await server.live.quickCreate({ permanentLive: false, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) | ||
110 | live = data.live | ||
111 | video = data.video | ||
112 | |||
113 | ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
114 | await waitJobs([ server ]) | ||
115 | |||
116 | const job = await server.runnerJobs.requestLiveJob(runnerToken) | ||
117 | jobUUID = job.uuid | ||
118 | |||
119 | expect(job.type).to.equal('live-rtmp-hls-transcoding') | ||
120 | expect(job.payload.input.rtmpUrl).to.exist | ||
121 | |||
122 | expect(job.payload.output.toTranscode).to.have.lengthOf(5) | ||
123 | |||
124 | for (const { resolution, fps } of job.payload.output.toTranscode) { | ||
125 | expect([ 720, 480, 360, 240, 144 ]).to.contain(resolution) | ||
126 | |||
127 | expect(fps).to.be.above(25) | ||
128 | expect(fps).to.be.below(70) | ||
129 | } | ||
130 | }) | ||
131 | |||
132 | it('Should update the live with a new chunk', async function () { | ||
133 | this.timeout(120000) | ||
134 | |||
135 | const { job } = await server.runnerJobs.accept<RunnerJobLiveRTMPHLSTranscodingPayload>({ jobUUID, runnerToken }) | ||
136 | acceptedJob = job | ||
137 | |||
138 | { | ||
139 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
140 | masterPlaylistFile: 'live/master.m3u8', | ||
141 | resolutionPlaylistFile: 'live/0.m3u8', | ||
142 | resolutionPlaylistFilename: '0.m3u8', | ||
143 | type: 'add-chunk', | ||
144 | videoChunkFile: 'live/0-000067.ts', | ||
145 | videoChunkFilename: '0-000067.ts' | ||
146 | } | ||
147 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload, progress: 50 }) | ||
148 | |||
149 | const updatedJob = await server.runnerJobs.getJob({ uuid: job.uuid }) | ||
150 | expect(updatedJob.progress).to.equal(50) | ||
151 | } | ||
152 | |||
153 | { | ||
154 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
155 | resolutionPlaylistFile: 'live/1.m3u8', | ||
156 | resolutionPlaylistFilename: '1.m3u8', | ||
157 | type: 'add-chunk', | ||
158 | videoChunkFile: 'live/1-000068.ts', | ||
159 | videoChunkFilename: '1-000068.ts' | ||
160 | } | ||
161 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload }) | ||
162 | } | ||
163 | |||
164 | await wait(1000) | ||
165 | |||
166 | await testPlaylistFile('master.m3u8', 'live/master.m3u8') | ||
167 | await testPlaylistFile('0.m3u8', 'live/0.m3u8') | ||
168 | await testPlaylistFile('1.m3u8', 'live/1.m3u8') | ||
169 | |||
170 | await testTSFile('0-000067.ts', 'live/0-000067.ts') | ||
171 | await testTSFile('1-000068.ts', 'live/1-000068.ts') | ||
172 | }) | ||
173 | |||
174 | it('Should replace existing m3u8 on update', async function () { | ||
175 | this.timeout(120000) | ||
176 | |||
177 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
178 | masterPlaylistFile: 'live/1.m3u8', | ||
179 | resolutionPlaylistFilename: '0.m3u8', | ||
180 | resolutionPlaylistFile: 'live/1.m3u8', | ||
181 | type: 'add-chunk', | ||
182 | videoChunkFile: 'live/1-000069.ts', | ||
183 | videoChunkFilename: '1-000068.ts' | ||
184 | } | ||
185 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload }) | ||
186 | await wait(1000) | ||
187 | |||
188 | await testPlaylistFile('master.m3u8', 'live/1.m3u8') | ||
189 | await testPlaylistFile('0.m3u8', 'live/1.m3u8') | ||
190 | await testTSFile('1-000068.ts', 'live/1-000069.ts') | ||
191 | }) | ||
192 | |||
193 | it('Should update the live with removed chunks', async function () { | ||
194 | this.timeout(120000) | ||
195 | |||
196 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
197 | resolutionPlaylistFile: 'live/0.m3u8', | ||
198 | resolutionPlaylistFilename: '0.m3u8', | ||
199 | type: 'remove-chunk', | ||
200 | videoChunkFilename: '1-000068.ts' | ||
201 | } | ||
202 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload }) | ||
203 | |||
204 | await wait(1000) | ||
205 | |||
206 | await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/master.m3u8` }) | ||
207 | await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/0.m3u8` }) | ||
208 | await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/1.m3u8` }) | ||
209 | await makeRawRequest({ url: `${baseUrl}/${video.uuid}/0-000067.ts`, expectedStatus: HttpStatusCode.OK_200 }) | ||
210 | await makeRawRequest({ url: `${baseUrl}/${video.uuid}/1-000068.ts`, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
211 | }) | ||
212 | |||
213 | it('Should complete the live and save the replay', async function () { | ||
214 | this.timeout(120000) | ||
215 | |||
216 | for (const segment of [ '0-000069.ts', '0-000070.ts' ]) { | ||
217 | const payload: LiveRTMPHLSTranscodingUpdatePayload = { | ||
218 | masterPlaylistFile: 'live/master.m3u8', | ||
219 | resolutionPlaylistFilename: '0.m3u8', | ||
220 | resolutionPlaylistFile: 'live/0.m3u8', | ||
221 | type: 'add-chunk', | ||
222 | videoChunkFile: 'live/' + segment, | ||
223 | videoChunkFilename: segment | ||
224 | } | ||
225 | await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload }) | ||
226 | |||
227 | await wait(1000) | ||
228 | } | ||
229 | |||
230 | await waitJobs([ server ]) | ||
231 | |||
232 | { | ||
233 | const { state } = await server.videos.get({ id: video.uuid }) | ||
234 | expect(state.id).to.equal(VideoState.PUBLISHED) | ||
235 | } | ||
236 | |||
237 | await stopFfmpeg(ffmpegCommand) | ||
238 | |||
239 | await server.runnerJobs.success({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload: {} }) | ||
240 | |||
241 | await wait(1500) | ||
242 | await waitJobs([ server ]) | ||
243 | |||
244 | { | ||
245 | const { state } = await server.videos.get({ id: video.uuid }) | ||
246 | expect(state.id).to.equal(VideoState.LIVE_ENDED) | ||
247 | |||
248 | const session = await server.live.findLatestSession({ videoId: video.uuid }) | ||
249 | expect(session.error).to.be.null | ||
250 | } | ||
251 | }) | ||
252 | }) | ||
253 | |||
254 | describe('With transcoding enabled on cancelled/aborted/errored live', function () { | ||
255 | let live: LiveVideo | ||
256 | let video: Video | ||
257 | let ffmpegCommand: FfmpegCommand | ||
258 | |||
259 | async function prepare () { | ||
260 | ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
261 | await server.runnerJobs.requestLiveJob(runnerToken) | ||
262 | |||
263 | const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' }) | ||
264 | |||
265 | return job | ||
266 | } | ||
267 | |||
268 | async function checkSessionError (error: LiveVideoErrorType) { | ||
269 | await wait(1500) | ||
270 | await waitJobs([ server ]) | ||
271 | |||
272 | const session = await server.live.findLatestSession({ videoId: video.uuid }) | ||
273 | expect(session.error).to.equal(error) | ||
274 | } | ||
275 | |||
276 | before(async function () { | ||
277 | await server.config.enableLive({ | ||
278 | allowReplay: true, | ||
279 | resolutions: 'max', | ||
280 | transcoding: true | ||
281 | }) | ||
282 | |||
283 | const data = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) | ||
284 | live = data.live | ||
285 | video = data.video | ||
286 | }) | ||
287 | |||
288 | it('Should abort a running live', async function () { | ||
289 | this.timeout(120000) | ||
290 | |||
291 | const job = await prepare() | ||
292 | |||
293 | await Promise.all([ | ||
294 | server.runnerJobs.abort({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, reason: 'abort' }), | ||
295 | testFfmpegStreamError(ffmpegCommand, true) | ||
296 | ]) | ||
297 | |||
298 | // Abort is not supported | ||
299 | await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR) | ||
300 | }) | ||
301 | |||
302 | it('Should cancel a running live', async function () { | ||
303 | this.timeout(120000) | ||
304 | |||
305 | const job = await prepare() | ||
306 | |||
307 | await Promise.all([ | ||
308 | server.runnerJobs.cancelByAdmin({ jobUUID: job.uuid }), | ||
309 | testFfmpegStreamError(ffmpegCommand, true) | ||
310 | ]) | ||
311 | |||
312 | await checkSessionError(LiveVideoError.RUNNER_JOB_CANCEL) | ||
313 | }) | ||
314 | |||
315 | it('Should error a running live', async function () { | ||
316 | this.timeout(120000) | ||
317 | |||
318 | const job = await prepare() | ||
319 | |||
320 | await Promise.all([ | ||
321 | server.runnerJobs.error({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, message: 'error' }), | ||
322 | testFfmpegStreamError(ffmpegCommand, true) | ||
323 | ]) | ||
324 | |||
325 | await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR) | ||
326 | }) | ||
327 | }) | ||
328 | |||
329 | after(async function () { | ||
330 | await cleanupTests([ server ]) | ||
331 | }) | ||
332 | }) | ||
diff --git a/packages/tests/src/api/runners/runner-socket.ts b/packages/tests/src/api/runners/runner-socket.ts new file mode 100644 index 000000000..726ef084f --- /dev/null +++ b/packages/tests/src/api/runners/runner-socket.ts | |||
@@ -0,0 +1,120 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test runner socket', function () { | ||
15 | let server: PeerTubeServer | ||
16 | let runnerToken: string | ||
17 | |||
18 | before(async function () { | ||
19 | this.timeout(120_000) | ||
20 | |||
21 | server = await createSingleServer(1) | ||
22 | |||
23 | await setAccessTokensToServers([ server ]) | ||
24 | await setDefaultVideoChannel([ server ]) | ||
25 | |||
26 | await server.config.enableTranscoding({ hls: true, webVideo: true }) | ||
27 | await server.config.enableRemoteTranscoding() | ||
28 | runnerToken = await server.runners.autoRegisterRunner() | ||
29 | }) | ||
30 | |||
31 | it('Should throw an error without runner token', function (done) { | ||
32 | const localSocket = server.socketIO.getRunnersSocket({ runnerToken: null }) | ||
33 | localSocket.on('connect_error', err => { | ||
34 | expect(err.message).to.contain('No runner token provided') | ||
35 | done() | ||
36 | }) | ||
37 | }) | ||
38 | |||
39 | it('Should throw an error with a bad runner token', function (done) { | ||
40 | const localSocket = server.socketIO.getRunnersSocket({ runnerToken: 'ergag' }) | ||
41 | localSocket.on('connect_error', err => { | ||
42 | expect(err.message).to.contain('Invalid runner token') | ||
43 | done() | ||
44 | }) | ||
45 | }) | ||
46 | |||
47 | it('Should not send ping if there is no available jobs', async function () { | ||
48 | let pings = 0 | ||
49 | const localSocket = server.socketIO.getRunnersSocket({ runnerToken }) | ||
50 | localSocket.on('available-jobs', () => pings++) | ||
51 | |||
52 | expect(pings).to.equal(0) | ||
53 | }) | ||
54 | |||
55 | it('Should send a ping on available job', async function () { | ||
56 | let pings = 0 | ||
57 | const localSocket = server.socketIO.getRunnersSocket({ runnerToken }) | ||
58 | localSocket.on('available-jobs', () => pings++) | ||
59 | |||
60 | await server.videos.quickUpload({ name: 'video1' }) | ||
61 | await waitJobs([ server ]) | ||
62 | |||
63 | // eslint-disable-next-line no-unmodified-loop-condition | ||
64 | while (pings !== 1) { | ||
65 | await wait(500) | ||
66 | } | ||
67 | |||
68 | await server.videos.quickUpload({ name: 'video2' }) | ||
69 | await waitJobs([ server ]) | ||
70 | |||
71 | // eslint-disable-next-line no-unmodified-loop-condition | ||
72 | while ((pings as number) !== 2) { | ||
73 | await wait(500) | ||
74 | } | ||
75 | |||
76 | await server.runnerJobs.cancelAllJobs() | ||
77 | }) | ||
78 | |||
79 | it('Should send a ping when a child is ready', async function () { | ||
80 | let pings = 0 | ||
81 | const localSocket = server.socketIO.getRunnersSocket({ runnerToken }) | ||
82 | localSocket.on('available-jobs', () => pings++) | ||
83 | |||
84 | await server.videos.quickUpload({ name: 'video3' }) | ||
85 | await waitJobs([ server ]) | ||
86 | |||
87 | // eslint-disable-next-line no-unmodified-loop-condition | ||
88 | while (pings !== 1) { | ||
89 | await wait(500) | ||
90 | } | ||
91 | |||
92 | await server.runnerJobs.autoProcessWebVideoJob(runnerToken) | ||
93 | await waitJobs([ server ]) | ||
94 | |||
95 | // eslint-disable-next-line no-unmodified-loop-condition | ||
96 | while ((pings as number) !== 2) { | ||
97 | await wait(500) | ||
98 | } | ||
99 | }) | ||
100 | |||
101 | it('Should not send a ping if the ended job does not have a child', async function () { | ||
102 | let pings = 0 | ||
103 | const localSocket = server.socketIO.getRunnersSocket({ runnerToken }) | ||
104 | localSocket.on('available-jobs', () => pings++) | ||
105 | |||
106 | const { availableJobs } = await server.runnerJobs.request({ runnerToken }) | ||
107 | const job = availableJobs.find(j => j.type === 'vod-web-video-transcoding') | ||
108 | await server.runnerJobs.autoProcessWebVideoJob(runnerToken, job.uuid) | ||
109 | |||
110 | // Wait for debounce | ||
111 | await wait(1000) | ||
112 | await waitJobs([ server ]) | ||
113 | |||
114 | expect(pings).to.equal(0) | ||
115 | }) | ||
116 | |||
117 | after(async function () { | ||
118 | await cleanupTests([ server ]) | ||
119 | }) | ||
120 | }) | ||
diff --git a/packages/tests/src/api/runners/runner-studio-transcoding.ts b/packages/tests/src/api/runners/runner-studio-transcoding.ts new file mode 100644 index 000000000..adf6941c3 --- /dev/null +++ b/packages/tests/src/api/runners/runner-studio-transcoding.ts | |||
@@ -0,0 +1,169 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { readFile } from 'fs/promises' | ||
5 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | RunnerJobStudioTranscodingPayload, | ||
8 | VideoStudioTranscodingSuccess, | ||
9 | VideoState, | ||
10 | VideoStudioTask, | ||
11 | VideoStudioTaskIntro | ||
12 | } from '@peertube/peertube-models' | ||
13 | import { | ||
14 | cleanupTests, | ||
15 | createMultipleServers, | ||
16 | doubleFollow, | ||
17 | PeerTubeServer, | ||
18 | setAccessTokensToServers, | ||
19 | setDefaultVideoChannel, | ||
20 | VideoStudioCommand, | ||
21 | waitJobs | ||
22 | } from '@peertube/peertube-server-commands' | ||
23 | import { checkVideoDuration } from '@tests/shared/checks.js' | ||
24 | import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js' | ||
25 | |||
26 | describe('Test runner video studio transcoding', function () { | ||
27 | let servers: PeerTubeServer[] = [] | ||
28 | let runnerToken: string | ||
29 | let videoUUID: string | ||
30 | let jobUUID: string | ||
31 | |||
32 | async function renewStudio (tasks: VideoStudioTask[] = VideoStudioCommand.getComplexTask()) { | ||
33 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
34 | videoUUID = uuid | ||
35 | |||
36 | await waitJobs(servers) | ||
37 | |||
38 | await servers[0].videoStudio.createEditionTasks({ videoId: uuid, tasks }) | ||
39 | await waitJobs(servers) | ||
40 | |||
41 | const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken }) | ||
42 | expect(availableJobs).to.have.lengthOf(1) | ||
43 | |||
44 | jobUUID = availableJobs[0].uuid | ||
45 | } | ||
46 | |||
47 | before(async function () { | ||
48 | this.timeout(120_000) | ||
49 | |||
50 | servers = await createMultipleServers(2) | ||
51 | |||
52 | await setAccessTokensToServers(servers) | ||
53 | await setDefaultVideoChannel(servers) | ||
54 | |||
55 | await doubleFollow(servers[0], servers[1]) | ||
56 | |||
57 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) | ||
58 | await servers[0].config.enableStudio() | ||
59 | await servers[0].config.enableRemoteStudio() | ||
60 | |||
61 | runnerToken = await servers[0].runners.autoRegisterRunner() | ||
62 | }) | ||
63 | |||
64 | it('Should error a studio transcoding job', async function () { | ||
65 | this.timeout(60000) | ||
66 | |||
67 | await renewStudio() | ||
68 | |||
69 | for (let i = 0; i < 5; i++) { | ||
70 | const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID }) | ||
71 | const jobToken = job.jobToken | ||
72 | |||
73 | await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' }) | ||
74 | } | ||
75 | |||
76 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
77 | expect(video.state.id).to.equal(VideoState.PUBLISHED) | ||
78 | |||
79 | await checkPersistentTmpIsEmpty(servers[0]) | ||
80 | }) | ||
81 | |||
82 | it('Should cancel a transcoding job', async function () { | ||
83 | this.timeout(60000) | ||
84 | |||
85 | await renewStudio() | ||
86 | |||
87 | await servers[0].runnerJobs.cancelByAdmin({ jobUUID }) | ||
88 | |||
89 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
90 | expect(video.state.id).to.equal(VideoState.PUBLISHED) | ||
91 | |||
92 | await checkPersistentTmpIsEmpty(servers[0]) | ||
93 | }) | ||
94 | |||
95 | it('Should execute a remote studio job', async function () { | ||
96 | this.timeout(240_000) | ||
97 | |||
98 | const tasks = [ | ||
99 | { | ||
100 | name: 'add-outro' as 'add-outro', | ||
101 | options: { | ||
102 | file: 'video_short.webm' | ||
103 | } | ||
104 | }, | ||
105 | { | ||
106 | name: 'add-watermark' as 'add-watermark', | ||
107 | options: { | ||
108 | file: 'custom-thumbnail.png' | ||
109 | } | ||
110 | }, | ||
111 | { | ||
112 | name: 'add-intro' as 'add-intro', | ||
113 | options: { | ||
114 | file: 'video_very_short_240p.mp4' | ||
115 | } | ||
116 | } | ||
117 | ] | ||
118 | |||
119 | await renewStudio(tasks) | ||
120 | |||
121 | for (const server of servers) { | ||
122 | await checkVideoDuration(server, videoUUID, 5) | ||
123 | } | ||
124 | |||
125 | const { job } = await servers[0].runnerJobs.accept<RunnerJobStudioTranscodingPayload>({ runnerToken, jobUUID }) | ||
126 | const jobToken = job.jobToken | ||
127 | |||
128 | expect(job.type === 'video-studio-transcoding') | ||
129 | expect(job.payload.input.videoFileUrl).to.exist | ||
130 | |||
131 | // Check video input file | ||
132 | { | ||
133 | await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken }) | ||
134 | } | ||
135 | |||
136 | // Check task files | ||
137 | for (let i = 0; i < tasks.length; i++) { | ||
138 | const task = tasks[i] | ||
139 | const payloadTask = job.payload.tasks[i] | ||
140 | |||
141 | expect(payloadTask.name).to.equal(task.name) | ||
142 | |||
143 | const inputFile = await readFile(buildAbsoluteFixturePath(task.options.file)) | ||
144 | |||
145 | const { body } = await servers[0].runnerJobs.getJobFile({ | ||
146 | url: (payloadTask as VideoStudioTaskIntro).options.file as string, | ||
147 | jobToken, | ||
148 | runnerToken | ||
149 | }) | ||
150 | |||
151 | expect(body).to.deep.equal(inputFile) | ||
152 | } | ||
153 | |||
154 | const payload: VideoStudioTranscodingSuccess = { videoFile: 'video_very_short_240p.mp4' } | ||
155 | await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
156 | |||
157 | await waitJobs(servers) | ||
158 | |||
159 | for (const server of servers) { | ||
160 | await checkVideoDuration(server, videoUUID, 2) | ||
161 | } | ||
162 | |||
163 | await checkPersistentTmpIsEmpty(servers[0]) | ||
164 | }) | ||
165 | |||
166 | after(async function () { | ||
167 | await cleanupTests(servers) | ||
168 | }) | ||
169 | }) | ||
diff --git a/packages/tests/src/api/runners/runner-vod-transcoding.ts b/packages/tests/src/api/runners/runner-vod-transcoding.ts new file mode 100644 index 000000000..fe1c8f0b2 --- /dev/null +++ b/packages/tests/src/api/runners/runner-vod-transcoding.ts | |||
@@ -0,0 +1,545 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { readFile } from 'fs/promises' | ||
5 | import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' | ||
6 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
7 | import { | ||
8 | HttpStatusCode, | ||
9 | RunnerJobSuccessPayload, | ||
10 | RunnerJobVODAudioMergeTranscodingPayload, | ||
11 | RunnerJobVODHLSTranscodingPayload, | ||
12 | RunnerJobVODPayload, | ||
13 | RunnerJobVODWebVideoTranscodingPayload, | ||
14 | VideoState, | ||
15 | VODAudioMergeTranscodingSuccess, | ||
16 | VODHLSTranscodingSuccess, | ||
17 | VODWebVideoTranscodingSuccess | ||
18 | } from '@peertube/peertube-models' | ||
19 | import { | ||
20 | cleanupTests, | ||
21 | createMultipleServers, | ||
22 | doubleFollow, | ||
23 | makeGetRequest, | ||
24 | makeRawRequest, | ||
25 | PeerTubeServer, | ||
26 | setAccessTokensToServers, | ||
27 | setDefaultVideoChannel, | ||
28 | waitJobs | ||
29 | } from '@peertube/peertube-server-commands' | ||
30 | |||
31 | async function processAllJobs (server: PeerTubeServer, runnerToken: string) { | ||
32 | do { | ||
33 | const { availableJobs } = await server.runnerJobs.requestVOD({ runnerToken }) | ||
34 | if (availableJobs.length === 0) break | ||
35 | |||
36 | const { job } = await server.runnerJobs.accept<RunnerJobVODPayload>({ runnerToken, jobUUID: availableJobs[0].uuid }) | ||
37 | |||
38 | const payload: RunnerJobSuccessPayload = { | ||
39 | videoFile: `video_short_${job.payload.output.resolution}p.mp4`, | ||
40 | resolutionPlaylistFile: `video_short_${job.payload.output.resolution}p.m3u8` | ||
41 | } | ||
42 | await server.runnerJobs.success({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, payload }) | ||
43 | } while (true) | ||
44 | |||
45 | await waitJobs([ server ]) | ||
46 | } | ||
47 | |||
48 | describe('Test runner VOD transcoding', function () { | ||
49 | let servers: PeerTubeServer[] = [] | ||
50 | let runnerToken: string | ||
51 | |||
52 | before(async function () { | ||
53 | this.timeout(120_000) | ||
54 | |||
55 | servers = await createMultipleServers(2) | ||
56 | |||
57 | await setAccessTokensToServers(servers) | ||
58 | await setDefaultVideoChannel(servers) | ||
59 | |||
60 | await doubleFollow(servers[0], servers[1]) | ||
61 | |||
62 | await servers[0].config.enableRemoteTranscoding() | ||
63 | runnerToken = await servers[0].runners.autoRegisterRunner() | ||
64 | }) | ||
65 | |||
66 | describe('Without transcoding', function () { | ||
67 | |||
68 | before(async function () { | ||
69 | this.timeout(60000) | ||
70 | |||
71 | await servers[0].config.disableTranscoding() | ||
72 | await servers[0].videos.quickUpload({ name: 'video' }) | ||
73 | |||
74 | await waitJobs(servers) | ||
75 | }) | ||
76 | |||
77 | it('Should not have available jobs', async function () { | ||
78 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
79 | expect(availableJobs).to.have.lengthOf(0) | ||
80 | }) | ||
81 | }) | ||
82 | |||
83 | describe('With classic transcoding enabled', function () { | ||
84 | |||
85 | before(async function () { | ||
86 | this.timeout(60000) | ||
87 | |||
88 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) | ||
89 | }) | ||
90 | |||
91 | it('Should error a transcoding job', async function () { | ||
92 | this.timeout(60000) | ||
93 | |||
94 | await servers[0].runnerJobs.cancelAllJobs() | ||
95 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
96 | await waitJobs(servers) | ||
97 | |||
98 | const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken }) | ||
99 | const jobUUID = availableJobs[0].uuid | ||
100 | |||
101 | for (let i = 0; i < 5; i++) { | ||
102 | const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID }) | ||
103 | const jobToken = job.jobToken | ||
104 | |||
105 | await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' }) | ||
106 | } | ||
107 | |||
108 | const video = await servers[0].videos.get({ id: uuid }) | ||
109 | expect(video.state.id).to.equal(VideoState.TRANSCODING_FAILED) | ||
110 | }) | ||
111 | |||
112 | it('Should cancel a transcoding job', async function () { | ||
113 | await servers[0].runnerJobs.cancelAllJobs() | ||
114 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
115 | await waitJobs(servers) | ||
116 | |||
117 | const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken }) | ||
118 | const jobUUID = availableJobs[0].uuid | ||
119 | |||
120 | await servers[0].runnerJobs.cancelByAdmin({ jobUUID }) | ||
121 | |||
122 | const video = await servers[0].videos.get({ id: uuid }) | ||
123 | expect(video.state.id).to.equal(VideoState.PUBLISHED) | ||
124 | }) | ||
125 | }) | ||
126 | |||
127 | describe('Web video transcoding only', function () { | ||
128 | let videoUUID: string | ||
129 | let jobToken: string | ||
130 | let jobUUID: string | ||
131 | |||
132 | before(async function () { | ||
133 | this.timeout(60000) | ||
134 | |||
135 | await servers[0].runnerJobs.cancelAllJobs() | ||
136 | await servers[0].config.enableTranscoding({ hls: false, webVideo: true }) | ||
137 | |||
138 | const { uuid } = await servers[0].videos.quickUpload({ name: 'web video', fixture: 'video_short.webm' }) | ||
139 | videoUUID = uuid | ||
140 | |||
141 | await waitJobs(servers) | ||
142 | }) | ||
143 | |||
144 | it('Should have jobs available for remote runners', async function () { | ||
145 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
146 | expect(availableJobs).to.have.lengthOf(1) | ||
147 | |||
148 | jobUUID = availableJobs[0].uuid | ||
149 | }) | ||
150 | |||
151 | it('Should have a valid first transcoding job', async function () { | ||
152 | const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID }) | ||
153 | jobToken = job.jobToken | ||
154 | |||
155 | expect(job.type === 'vod-web-video-transcoding') | ||
156 | expect(job.payload.input.videoFileUrl).to.exist | ||
157 | expect(job.payload.output.resolution).to.equal(720) | ||
158 | expect(job.payload.output.fps).to.equal(25) | ||
159 | |||
160 | const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken }) | ||
161 | const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm')) | ||
162 | |||
163 | expect(body).to.deep.equal(inputFile) | ||
164 | }) | ||
165 | |||
166 | it('Should transcode the max video resolution and send it back to the server', async function () { | ||
167 | this.timeout(60000) | ||
168 | |||
169 | const payload: VODWebVideoTranscodingSuccess = { | ||
170 | videoFile: 'video_short.mp4' | ||
171 | } | ||
172 | await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
173 | |||
174 | await waitJobs(servers) | ||
175 | }) | ||
176 | |||
177 | it('Should have the video updated', async function () { | ||
178 | for (const server of servers) { | ||
179 | const video = await server.videos.get({ id: videoUUID }) | ||
180 | expect(video.files).to.have.lengthOf(1) | ||
181 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
182 | |||
183 | const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
184 | expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4'))) | ||
185 | } | ||
186 | }) | ||
187 | |||
188 | it('Should have 4 lower resolution to transcode', async function () { | ||
189 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
190 | expect(availableJobs).to.have.lengthOf(4) | ||
191 | |||
192 | for (const resolution of [ 480, 360, 240, 144 ]) { | ||
193 | const job = availableJobs.find(j => j.payload.output.resolution === resolution) | ||
194 | expect(job).to.exist | ||
195 | expect(job.type).to.equal('vod-web-video-transcoding') | ||
196 | |||
197 | if (resolution === 240) jobUUID = job.uuid | ||
198 | } | ||
199 | }) | ||
200 | |||
201 | it('Should process one of these transcoding jobs', async function () { | ||
202 | const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID }) | ||
203 | jobToken = job.jobToken | ||
204 | |||
205 | const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken }) | ||
206 | const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4')) | ||
207 | |||
208 | expect(body).to.deep.equal(inputFile) | ||
209 | |||
210 | const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${job.payload.output.resolution}p.mp4` } | ||
211 | await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
212 | }) | ||
213 | |||
214 | it('Should process all other jobs', async function () { | ||
215 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
216 | expect(availableJobs).to.have.lengthOf(3) | ||
217 | |||
218 | for (const resolution of [ 480, 360, 144 ]) { | ||
219 | const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution) | ||
220 | expect(availableJob).to.exist | ||
221 | jobUUID = availableJob.uuid | ||
222 | |||
223 | const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID }) | ||
224 | jobToken = job.jobToken | ||
225 | |||
226 | const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken }) | ||
227 | const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4')) | ||
228 | expect(body).to.deep.equal(inputFile) | ||
229 | |||
230 | const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${resolution}p.mp4` } | ||
231 | await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
232 | } | ||
233 | |||
234 | await waitJobs(servers) | ||
235 | }) | ||
236 | |||
237 | it('Should have the video updated', async function () { | ||
238 | for (const server of servers) { | ||
239 | const video = await server.videos.get({ id: videoUUID }) | ||
240 | expect(video.files).to.have.lengthOf(5) | ||
241 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
242 | |||
243 | const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
244 | expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4'))) | ||
245 | |||
246 | for (const file of video.files) { | ||
247 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
248 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
249 | } | ||
250 | } | ||
251 | }) | ||
252 | |||
253 | it('Should not have available jobs anymore', async function () { | ||
254 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
255 | expect(availableJobs).to.have.lengthOf(0) | ||
256 | }) | ||
257 | }) | ||
258 | |||
259 | describe('HLS transcoding only', function () { | ||
260 | let videoUUID: string | ||
261 | let jobToken: string | ||
262 | let jobUUID: string | ||
263 | |||
264 | before(async function () { | ||
265 | this.timeout(60000) | ||
266 | |||
267 | await servers[0].config.enableTranscoding({ hls: true, webVideo: false }) | ||
268 | |||
269 | const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' }) | ||
270 | videoUUID = uuid | ||
271 | |||
272 | await waitJobs(servers) | ||
273 | }) | ||
274 | |||
275 | it('Should run the optimize job', async function () { | ||
276 | this.timeout(60000) | ||
277 | |||
278 | await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken) | ||
279 | }) | ||
280 | |||
281 | it('Should have 5 HLS resolution to transcode', async function () { | ||
282 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
283 | expect(availableJobs).to.have.lengthOf(5) | ||
284 | |||
285 | for (const resolution of [ 720, 480, 360, 240, 144 ]) { | ||
286 | const job = availableJobs.find(j => j.payload.output.resolution === resolution) | ||
287 | expect(job).to.exist | ||
288 | expect(job.type).to.equal('vod-hls-transcoding') | ||
289 | |||
290 | if (resolution === 480) jobUUID = job.uuid | ||
291 | } | ||
292 | }) | ||
293 | |||
294 | it('Should process one of these transcoding jobs', async function () { | ||
295 | this.timeout(60000) | ||
296 | |||
297 | const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID }) | ||
298 | jobToken = job.jobToken | ||
299 | |||
300 | const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken }) | ||
301 | const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4')) | ||
302 | |||
303 | expect(body).to.deep.equal(inputFile) | ||
304 | |||
305 | const payload: VODHLSTranscodingSuccess = { | ||
306 | videoFile: 'video_short_480p.mp4', | ||
307 | resolutionPlaylistFile: 'video_short_480p.m3u8' | ||
308 | } | ||
309 | await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
310 | |||
311 | await waitJobs(servers) | ||
312 | }) | ||
313 | |||
314 | it('Should have the video updated', async function () { | ||
315 | for (const server of servers) { | ||
316 | const video = await server.videos.get({ id: videoUUID }) | ||
317 | |||
318 | expect(video.files).to.have.lengthOf(1) | ||
319 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
320 | |||
321 | const hls = video.streamingPlaylists[0] | ||
322 | expect(hls.files).to.have.lengthOf(1) | ||
323 | |||
324 | await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] }) | ||
325 | } | ||
326 | }) | ||
327 | |||
328 | it('Should process all other jobs', async function () { | ||
329 | this.timeout(60000) | ||
330 | |||
331 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
332 | expect(availableJobs).to.have.lengthOf(4) | ||
333 | |||
334 | let maxQualityFile = 'video_short.mp4' | ||
335 | |||
336 | for (const resolution of [ 720, 360, 240, 144 ]) { | ||
337 | const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution) | ||
338 | expect(availableJob).to.exist | ||
339 | jobUUID = availableJob.uuid | ||
340 | |||
341 | const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID }) | ||
342 | jobToken = job.jobToken | ||
343 | |||
344 | const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken }) | ||
345 | const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile)) | ||
346 | expect(body).to.deep.equal(inputFile) | ||
347 | |||
348 | const payload: VODHLSTranscodingSuccess = { | ||
349 | videoFile: `video_short_${resolution}p.mp4`, | ||
350 | resolutionPlaylistFile: `video_short_${resolution}p.m3u8` | ||
351 | } | ||
352 | await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
353 | |||
354 | if (resolution === 720) { | ||
355 | maxQualityFile = 'video_short_720p.mp4' | ||
356 | } | ||
357 | } | ||
358 | |||
359 | await waitJobs(servers) | ||
360 | }) | ||
361 | |||
362 | it('Should have the video updated', async function () { | ||
363 | for (const server of servers) { | ||
364 | const video = await server.videos.get({ id: videoUUID }) | ||
365 | |||
366 | expect(video.files).to.have.lengthOf(0) | ||
367 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
368 | |||
369 | const hls = video.streamingPlaylists[0] | ||
370 | expect(hls.files).to.have.lengthOf(5) | ||
371 | |||
372 | await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] }) | ||
373 | } | ||
374 | }) | ||
375 | |||
376 | it('Should not have available jobs anymore', async function () { | ||
377 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
378 | expect(availableJobs).to.have.lengthOf(0) | ||
379 | }) | ||
380 | }) | ||
381 | |||
382 | describe('Web video and HLS transcoding', function () { | ||
383 | |||
384 | before(async function () { | ||
385 | this.timeout(60000) | ||
386 | |||
387 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) | ||
388 | |||
389 | await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' }) | ||
390 | |||
391 | await waitJobs(servers) | ||
392 | }) | ||
393 | |||
394 | it('Should process the first optimize job', async function () { | ||
395 | this.timeout(60000) | ||
396 | |||
397 | await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken) | ||
398 | }) | ||
399 | |||
400 | it('Should have 9 jobs to process', async function () { | ||
401 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
402 | |||
403 | expect(availableJobs).to.have.lengthOf(9) | ||
404 | |||
405 | const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding') | ||
406 | const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding') | ||
407 | |||
408 | expect(webVideoJobs).to.have.lengthOf(4) | ||
409 | expect(hlsJobs).to.have.lengthOf(5) | ||
410 | }) | ||
411 | |||
412 | it('Should process all available jobs', async function () { | ||
413 | await processAllJobs(servers[0], runnerToken) | ||
414 | }) | ||
415 | }) | ||
416 | |||
417 | describe('Audio merge transcoding', function () { | ||
418 | let videoUUID: string | ||
419 | let jobToken: string | ||
420 | let jobUUID: string | ||
421 | |||
422 | before(async function () { | ||
423 | this.timeout(60000) | ||
424 | |||
425 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) | ||
426 | |||
427 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } | ||
428 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' }) | ||
429 | videoUUID = uuid | ||
430 | |||
431 | await waitJobs(servers) | ||
432 | }) | ||
433 | |||
434 | it('Should have an audio merge transcoding job', async function () { | ||
435 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
436 | expect(availableJobs).to.have.lengthOf(1) | ||
437 | |||
438 | expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding') | ||
439 | |||
440 | jobUUID = availableJobs[0].uuid | ||
441 | }) | ||
442 | |||
443 | it('Should have a valid remote audio merge transcoding job', async function () { | ||
444 | const { job } = await servers[0].runnerJobs.accept<RunnerJobVODAudioMergeTranscodingPayload>({ runnerToken, jobUUID }) | ||
445 | jobToken = job.jobToken | ||
446 | |||
447 | expect(job.type === 'vod-audio-merge-transcoding') | ||
448 | expect(job.payload.input.audioFileUrl).to.exist | ||
449 | expect(job.payload.input.previewFileUrl).to.exist | ||
450 | expect(job.payload.output.resolution).to.equal(480) | ||
451 | |||
452 | { | ||
453 | const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken }) | ||
454 | const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg')) | ||
455 | expect(body).to.deep.equal(inputFile) | ||
456 | } | ||
457 | |||
458 | { | ||
459 | const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken }) | ||
460 | |||
461 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
462 | const { body: inputFile } = await makeGetRequest({ | ||
463 | url: servers[0].url, | ||
464 | path: video.previewPath, | ||
465 | expectedStatus: HttpStatusCode.OK_200 | ||
466 | }) | ||
467 | |||
468 | expect(body).to.deep.equal(inputFile) | ||
469 | } | ||
470 | }) | ||
471 | |||
472 | it('Should merge the audio', async function () { | ||
473 | this.timeout(60000) | ||
474 | |||
475 | const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' } | ||
476 | await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
477 | |||
478 | await waitJobs(servers) | ||
479 | }) | ||
480 | |||
481 | it('Should have the video updated', async function () { | ||
482 | for (const server of servers) { | ||
483 | const video = await server.videos.get({ id: videoUUID }) | ||
484 | expect(video.files).to.have.lengthOf(1) | ||
485 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
486 | |||
487 | const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
488 | expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))) | ||
489 | } | ||
490 | }) | ||
491 | |||
492 | it('Should have 7 lower resolutions to transcode', async function () { | ||
493 | const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken }) | ||
494 | expect(availableJobs).to.have.lengthOf(7) | ||
495 | |||
496 | for (const resolution of [ 360, 240, 144 ]) { | ||
497 | const jobs = availableJobs.filter(j => j.payload.output.resolution === resolution) | ||
498 | expect(jobs).to.have.lengthOf(2) | ||
499 | } | ||
500 | |||
501 | jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid | ||
502 | }) | ||
503 | |||
504 | it('Should process one other job', async function () { | ||
505 | this.timeout(60000) | ||
506 | |||
507 | const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID }) | ||
508 | jobToken = job.jobToken | ||
509 | |||
510 | const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken }) | ||
511 | const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4')) | ||
512 | expect(body).to.deep.equal(inputFile) | ||
513 | |||
514 | const payload: VODHLSTranscodingSuccess = { | ||
515 | videoFile: `video_short_480p.mp4`, | ||
516 | resolutionPlaylistFile: `video_short_480p.m3u8` | ||
517 | } | ||
518 | await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) | ||
519 | |||
520 | await waitJobs(servers) | ||
521 | }) | ||
522 | |||
523 | it('Should have the video updated', async function () { | ||
524 | for (const server of servers) { | ||
525 | const video = await server.videos.get({ id: videoUUID }) | ||
526 | |||
527 | expect(video.files).to.have.lengthOf(1) | ||
528 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
529 | |||
530 | const hls = video.streamingPlaylists[0] | ||
531 | expect(hls.files).to.have.lengthOf(1) | ||
532 | |||
533 | await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] }) | ||
534 | } | ||
535 | }) | ||
536 | |||
537 | it('Should process all available jobs', async function () { | ||
538 | await processAllJobs(servers[0], runnerToken) | ||
539 | }) | ||
540 | }) | ||
541 | |||
542 | after(async function () { | ||
543 | await cleanupTests(servers) | ||
544 | }) | ||
545 | }) | ||
diff --git a/packages/tests/src/api/search/index.ts b/packages/tests/src/api/search/index.ts new file mode 100644 index 000000000..f4420261d --- /dev/null +++ b/packages/tests/src/api/search/index.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import './search-activitypub-video-playlists.js' | ||
2 | import './search-activitypub-video-channels.js' | ||
3 | import './search-activitypub-videos.js' | ||
4 | import './search-channels.js' | ||
5 | import './search-index.js' | ||
6 | import './search-playlists.js' | ||
7 | import './search-videos.js' | ||
diff --git a/packages/tests/src/api/search/search-activitypub-video-channels.ts b/packages/tests/src/api/search/search-activitypub-video-channels.ts new file mode 100644 index 000000000..b63f45b18 --- /dev/null +++ b/packages/tests/src/api/search/search-activitypub-video-channels.ts | |||
@@ -0,0 +1,255 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoChannel } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test ActivityPub video channels search', function () { | ||
18 | let servers: PeerTubeServer[] | ||
19 | let userServer2Token: string | ||
20 | let videoServer2UUID: string | ||
21 | let channelIdServer2: number | ||
22 | let command: SearchCommand | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(120000) | ||
26 | |||
27 | servers = await createMultipleServers(2) | ||
28 | |||
29 | await setAccessTokensToServers(servers) | ||
30 | await setDefaultVideoChannel(servers) | ||
31 | await setDefaultAccountAvatar(servers) | ||
32 | |||
33 | { | ||
34 | await servers[0].users.create({ username: 'user1_server1', password: 'password' }) | ||
35 | const channel = { | ||
36 | name: 'channel1_server1', | ||
37 | displayName: 'Channel 1 server 1' | ||
38 | } | ||
39 | await servers[0].channels.create({ attributes: channel }) | ||
40 | } | ||
41 | |||
42 | { | ||
43 | const user = { username: 'user1_server2', password: 'password' } | ||
44 | await servers[1].users.create({ username: user.username, password: user.password }) | ||
45 | userServer2Token = await servers[1].login.getAccessToken(user) | ||
46 | |||
47 | const channel = { | ||
48 | name: 'channel1_server2', | ||
49 | displayName: 'Channel 1 server 2' | ||
50 | } | ||
51 | const created = await servers[1].channels.create({ token: userServer2Token, attributes: channel }) | ||
52 | channelIdServer2 = created.id | ||
53 | |||
54 | const attributes = { name: 'video 1 server 2', channelId: channelIdServer2 } | ||
55 | const { uuid } = await servers[1].videos.upload({ token: userServer2Token, attributes }) | ||
56 | videoServer2UUID = uuid | ||
57 | } | ||
58 | |||
59 | await waitJobs(servers) | ||
60 | |||
61 | command = servers[0].search | ||
62 | }) | ||
63 | |||
64 | it('Should not find a remote video channel', async function () { | ||
65 | this.timeout(15000) | ||
66 | |||
67 | { | ||
68 | const search = servers[1].url + '/video-channels/channel1_server3' | ||
69 | const body = await command.searchChannels({ search, token: servers[0].accessToken }) | ||
70 | |||
71 | expect(body.total).to.equal(0) | ||
72 | expect(body.data).to.be.an('array') | ||
73 | expect(body.data).to.have.lengthOf(0) | ||
74 | } | ||
75 | |||
76 | { | ||
77 | // Without token | ||
78 | const search = servers[1].url + '/video-channels/channel1_server2' | ||
79 | const body = await command.searchChannels({ search }) | ||
80 | |||
81 | expect(body.total).to.equal(0) | ||
82 | expect(body.data).to.be.an('array') | ||
83 | expect(body.data).to.have.lengthOf(0) | ||
84 | } | ||
85 | }) | ||
86 | |||
87 | it('Should search a local video channel', async function () { | ||
88 | const searches = [ | ||
89 | servers[0].url + '/video-channels/channel1_server1', | ||
90 | 'channel1_server1@' + servers[0].host | ||
91 | ] | ||
92 | |||
93 | for (const search of searches) { | ||
94 | const body = await command.searchChannels({ search }) | ||
95 | |||
96 | expect(body.total).to.equal(1) | ||
97 | expect(body.data).to.be.an('array') | ||
98 | expect(body.data).to.have.lengthOf(1) | ||
99 | expect(body.data[0].name).to.equal('channel1_server1') | ||
100 | expect(body.data[0].displayName).to.equal('Channel 1 server 1') | ||
101 | } | ||
102 | }) | ||
103 | |||
104 | it('Should search a local video channel with an alternative URL', async function () { | ||
105 | const search = servers[0].url + '/c/channel1_server1' | ||
106 | |||
107 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
108 | const body = await command.searchChannels({ search, token }) | ||
109 | |||
110 | expect(body.total).to.equal(1) | ||
111 | expect(body.data).to.be.an('array') | ||
112 | expect(body.data).to.have.lengthOf(1) | ||
113 | expect(body.data[0].name).to.equal('channel1_server1') | ||
114 | expect(body.data[0].displayName).to.equal('Channel 1 server 1') | ||
115 | } | ||
116 | }) | ||
117 | |||
118 | it('Should search a local video channel with a query in URL', async function () { | ||
119 | const searches = [ | ||
120 | servers[0].url + '/video-channels/channel1_server1', | ||
121 | servers[0].url + '/c/channel1_server1' | ||
122 | ] | ||
123 | |||
124 | for (const search of searches) { | ||
125 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
126 | const body = await command.searchChannels({ search: search + '?param=2', token }) | ||
127 | |||
128 | expect(body.total).to.equal(1) | ||
129 | expect(body.data).to.be.an('array') | ||
130 | expect(body.data).to.have.lengthOf(1) | ||
131 | expect(body.data[0].name).to.equal('channel1_server1') | ||
132 | expect(body.data[0].displayName).to.equal('Channel 1 server 1') | ||
133 | } | ||
134 | } | ||
135 | }) | ||
136 | |||
137 | it('Should search a remote video channel with URL or handle', async function () { | ||
138 | const searches = [ | ||
139 | servers[1].url + '/video-channels/channel1_server2', | ||
140 | servers[1].url + '/c/channel1_server2', | ||
141 | servers[1].url + '/c/channel1_server2/videos', | ||
142 | 'channel1_server2@' + servers[1].host | ||
143 | ] | ||
144 | |||
145 | for (const search of searches) { | ||
146 | const body = await command.searchChannels({ search, token: servers[0].accessToken }) | ||
147 | |||
148 | expect(body.total).to.equal(1) | ||
149 | expect(body.data).to.be.an('array') | ||
150 | expect(body.data).to.have.lengthOf(1) | ||
151 | expect(body.data[0].name).to.equal('channel1_server2') | ||
152 | expect(body.data[0].displayName).to.equal('Channel 1 server 2') | ||
153 | } | ||
154 | }) | ||
155 | |||
156 | it('Should not list this remote video channel', async function () { | ||
157 | const body = await servers[0].channels.list() | ||
158 | expect(body.total).to.equal(3) | ||
159 | expect(body.data).to.have.lengthOf(3) | ||
160 | expect(body.data[0].name).to.equal('channel1_server1') | ||
161 | expect(body.data[1].name).to.equal('user1_server1_channel') | ||
162 | expect(body.data[2].name).to.equal('root_channel') | ||
163 | }) | ||
164 | |||
165 | it('Should list video channel videos of server 2 without token', async function () { | ||
166 | this.timeout(30000) | ||
167 | |||
168 | await waitJobs(servers) | ||
169 | |||
170 | const { total, data } = await servers[0].videos.listByChannel({ | ||
171 | token: null, | ||
172 | handle: 'channel1_server2@' + servers[1].host | ||
173 | }) | ||
174 | expect(total).to.equal(0) | ||
175 | expect(data).to.have.lengthOf(0) | ||
176 | }) | ||
177 | |||
178 | it('Should list video channel videos of server 2 with token', async function () { | ||
179 | const { total, data } = await servers[0].videos.listByChannel({ | ||
180 | handle: 'channel1_server2@' + servers[1].host | ||
181 | }) | ||
182 | |||
183 | expect(total).to.equal(1) | ||
184 | expect(data[0].name).to.equal('video 1 server 2') | ||
185 | }) | ||
186 | |||
187 | it('Should update video channel of server 2, and refresh it on server 1', async function () { | ||
188 | this.timeout(120000) | ||
189 | |||
190 | await servers[1].channels.update({ | ||
191 | token: userServer2Token, | ||
192 | channelName: 'channel1_server2', | ||
193 | attributes: { displayName: 'channel updated' } | ||
194 | }) | ||
195 | await servers[1].users.updateMe({ token: userServer2Token, displayName: 'user updated' }) | ||
196 | |||
197 | await waitJobs(servers) | ||
198 | // Expire video channel | ||
199 | await wait(10000) | ||
200 | |||
201 | const search = servers[1].url + '/video-channels/channel1_server2' | ||
202 | const body = await command.searchChannels({ search, token: servers[0].accessToken }) | ||
203 | expect(body.total).to.equal(1) | ||
204 | expect(body.data).to.have.lengthOf(1) | ||
205 | |||
206 | const videoChannel: VideoChannel = body.data[0] | ||
207 | expect(videoChannel.displayName).to.equal('channel updated') | ||
208 | |||
209 | // We don't return the owner account for now | ||
210 | // expect(videoChannel.ownerAccount.displayName).to.equal('user updated') | ||
211 | }) | ||
212 | |||
213 | it('Should update and add a video on server 2, and update it on server 1 after a search', async function () { | ||
214 | this.timeout(120000) | ||
215 | |||
216 | await servers[1].videos.update({ token: userServer2Token, id: videoServer2UUID, attributes: { name: 'video 1 updated' } }) | ||
217 | await servers[1].videos.upload({ token: userServer2Token, attributes: { name: 'video 2 server 2', channelId: channelIdServer2 } }) | ||
218 | |||
219 | await waitJobs(servers) | ||
220 | |||
221 | // Expire video channel | ||
222 | await wait(10000) | ||
223 | |||
224 | const search = servers[1].url + '/video-channels/channel1_server2' | ||
225 | await command.searchChannels({ search, token: servers[0].accessToken }) | ||
226 | |||
227 | await waitJobs(servers) | ||
228 | |||
229 | const handle = 'channel1_server2@' + servers[1].host | ||
230 | const { total, data } = await servers[0].videos.listByChannel({ handle, sort: '-createdAt' }) | ||
231 | |||
232 | expect(total).to.equal(2) | ||
233 | expect(data[0].name).to.equal('video 2 server 2') | ||
234 | expect(data[1].name).to.equal('video 1 updated') | ||
235 | }) | ||
236 | |||
237 | it('Should delete video channel of server 2, and delete it on server 1', async function () { | ||
238 | this.timeout(120000) | ||
239 | |||
240 | await servers[1].channels.delete({ token: userServer2Token, channelName: 'channel1_server2' }) | ||
241 | |||
242 | await waitJobs(servers) | ||
243 | // Expire video | ||
244 | await wait(10000) | ||
245 | |||
246 | const search = servers[1].url + '/video-channels/channel1_server2' | ||
247 | const body = await command.searchChannels({ search, token: servers[0].accessToken }) | ||
248 | expect(body.total).to.equal(0) | ||
249 | expect(body.data).to.have.lengthOf(0) | ||
250 | }) | ||
251 | |||
252 | after(async function () { | ||
253 | await cleanupTests(servers) | ||
254 | }) | ||
255 | }) | ||
diff --git a/packages/tests/src/api/search/search-activitypub-video-playlists.ts b/packages/tests/src/api/search/search-activitypub-video-playlists.ts new file mode 100644 index 000000000..33ecfd8e7 --- /dev/null +++ b/packages/tests/src/api/search/search-activitypub-video-playlists.ts | |||
@@ -0,0 +1,214 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test ActivityPub playlists search', function () { | ||
18 | let servers: PeerTubeServer[] | ||
19 | let playlistServer1UUID: string | ||
20 | let playlistServer2UUID: string | ||
21 | let video2Server2: string | ||
22 | |||
23 | let command: SearchCommand | ||
24 | |||
25 | before(async function () { | ||
26 | this.timeout(240000) | ||
27 | |||
28 | servers = await createMultipleServers(2) | ||
29 | |||
30 | await setAccessTokensToServers(servers) | ||
31 | await setDefaultVideoChannel(servers) | ||
32 | await setDefaultAccountAvatar(servers) | ||
33 | |||
34 | { | ||
35 | const video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid | ||
36 | const video2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).uuid | ||
37 | |||
38 | const attributes = { | ||
39 | displayName: 'playlist 1 on server 1', | ||
40 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
41 | videoChannelId: servers[0].store.channel.id | ||
42 | } | ||
43 | const created = await servers[0].playlists.create({ attributes }) | ||
44 | playlistServer1UUID = created.uuid | ||
45 | |||
46 | for (const videoId of [ video1, video2 ]) { | ||
47 | await servers[0].playlists.addElement({ playlistId: playlistServer1UUID, attributes: { videoId } }) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | { | ||
52 | const videoId = (await servers[1].videos.quickUpload({ name: 'video 1' })).uuid | ||
53 | video2Server2 = (await servers[1].videos.quickUpload({ name: 'video 2' })).uuid | ||
54 | |||
55 | const attributes = { | ||
56 | displayName: 'playlist 1 on server 2', | ||
57 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
58 | videoChannelId: servers[1].store.channel.id | ||
59 | } | ||
60 | const created = await servers[1].playlists.create({ attributes }) | ||
61 | playlistServer2UUID = created.uuid | ||
62 | |||
63 | await servers[1].playlists.addElement({ playlistId: playlistServer2UUID, attributes: { videoId } }) | ||
64 | } | ||
65 | |||
66 | await waitJobs(servers) | ||
67 | |||
68 | command = servers[0].search | ||
69 | }) | ||
70 | |||
71 | it('Should not find a remote playlist', async function () { | ||
72 | { | ||
73 | const search = servers[1].url + '/video-playlists/43' | ||
74 | const body = await command.searchPlaylists({ search, token: servers[0].accessToken }) | ||
75 | |||
76 | expect(body.total).to.equal(0) | ||
77 | expect(body.data).to.be.an('array') | ||
78 | expect(body.data).to.have.lengthOf(0) | ||
79 | } | ||
80 | |||
81 | { | ||
82 | // Without token | ||
83 | const search = servers[1].url + '/video-playlists/' + playlistServer2UUID | ||
84 | const body = await command.searchPlaylists({ search }) | ||
85 | |||
86 | expect(body.total).to.equal(0) | ||
87 | expect(body.data).to.be.an('array') | ||
88 | expect(body.data).to.have.lengthOf(0) | ||
89 | } | ||
90 | }) | ||
91 | |||
92 | it('Should search a local playlist', async function () { | ||
93 | const search = servers[0].url + '/video-playlists/' + playlistServer1UUID | ||
94 | const body = await command.searchPlaylists({ search }) | ||
95 | |||
96 | expect(body.total).to.equal(1) | ||
97 | expect(body.data).to.be.an('array') | ||
98 | expect(body.data).to.have.lengthOf(1) | ||
99 | expect(body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
100 | expect(body.data[0].videosLength).to.equal(2) | ||
101 | }) | ||
102 | |||
103 | it('Should search a local playlist with an alternative URL', async function () { | ||
104 | const searches = [ | ||
105 | servers[0].url + '/videos/watch/playlist/' + playlistServer1UUID, | ||
106 | servers[0].url + '/w/p/' + playlistServer1UUID | ||
107 | ] | ||
108 | |||
109 | for (const search of searches) { | ||
110 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
111 | const body = await command.searchPlaylists({ search, token }) | ||
112 | |||
113 | expect(body.total).to.equal(1) | ||
114 | expect(body.data).to.be.an('array') | ||
115 | expect(body.data).to.have.lengthOf(1) | ||
116 | expect(body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
117 | expect(body.data[0].videosLength).to.equal(2) | ||
118 | } | ||
119 | } | ||
120 | }) | ||
121 | |||
122 | it('Should search a local playlist with a query in URL', async function () { | ||
123 | const searches = [ | ||
124 | servers[0].url + '/videos/watch/playlist/' + playlistServer1UUID, | ||
125 | servers[0].url + '/w/p/' + playlistServer1UUID | ||
126 | ] | ||
127 | |||
128 | for (const search of searches) { | ||
129 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
130 | const body = await command.searchPlaylists({ search: search + '?param=1', token }) | ||
131 | |||
132 | expect(body.total).to.equal(1) | ||
133 | expect(body.data).to.be.an('array') | ||
134 | expect(body.data).to.have.lengthOf(1) | ||
135 | expect(body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
136 | expect(body.data[0].videosLength).to.equal(2) | ||
137 | } | ||
138 | } | ||
139 | }) | ||
140 | |||
141 | it('Should search a remote playlist', async function () { | ||
142 | const searches = [ | ||
143 | servers[1].url + '/video-playlists/' + playlistServer2UUID, | ||
144 | servers[1].url + '/videos/watch/playlist/' + playlistServer2UUID, | ||
145 | servers[1].url + '/w/p/' + playlistServer2UUID | ||
146 | ] | ||
147 | |||
148 | for (const search of searches) { | ||
149 | const body = await command.searchPlaylists({ search, token: servers[0].accessToken }) | ||
150 | |||
151 | expect(body.total).to.equal(1) | ||
152 | expect(body.data).to.be.an('array') | ||
153 | expect(body.data).to.have.lengthOf(1) | ||
154 | expect(body.data[0].displayName).to.equal('playlist 1 on server 2') | ||
155 | expect(body.data[0].videosLength).to.equal(1) | ||
156 | } | ||
157 | }) | ||
158 | |||
159 | it('Should not list this remote playlist', async function () { | ||
160 | const body = await servers[0].playlists.list({ start: 0, count: 10 }) | ||
161 | expect(body.total).to.equal(1) | ||
162 | expect(body.data).to.have.lengthOf(1) | ||
163 | expect(body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
164 | }) | ||
165 | |||
166 | it('Should update the playlist of server 2, and refresh it on server 1', async function () { | ||
167 | this.timeout(60000) | ||
168 | |||
169 | await servers[1].playlists.addElement({ playlistId: playlistServer2UUID, attributes: { videoId: video2Server2 } }) | ||
170 | |||
171 | await waitJobs(servers) | ||
172 | // Expire playlist | ||
173 | await wait(10000) | ||
174 | |||
175 | // Will run refresh async | ||
176 | const search = servers[1].url + '/video-playlists/' + playlistServer2UUID | ||
177 | await command.searchPlaylists({ search, token: servers[0].accessToken }) | ||
178 | |||
179 | // Wait refresh | ||
180 | await wait(5000) | ||
181 | |||
182 | const body = await command.searchPlaylists({ search, token: servers[0].accessToken }) | ||
183 | expect(body.total).to.equal(1) | ||
184 | expect(body.data).to.have.lengthOf(1) | ||
185 | |||
186 | const playlist = body.data[0] | ||
187 | expect(playlist.videosLength).to.equal(2) | ||
188 | }) | ||
189 | |||
190 | it('Should delete playlist of server 2, and delete it on server 1', async function () { | ||
191 | this.timeout(60000) | ||
192 | |||
193 | await servers[1].playlists.delete({ playlistId: playlistServer2UUID }) | ||
194 | |||
195 | await waitJobs(servers) | ||
196 | // Expiration | ||
197 | await wait(10000) | ||
198 | |||
199 | // Will run refresh async | ||
200 | const search = servers[1].url + '/video-playlists/' + playlistServer2UUID | ||
201 | await command.searchPlaylists({ search, token: servers[0].accessToken }) | ||
202 | |||
203 | // Wait refresh | ||
204 | await wait(5000) | ||
205 | |||
206 | const body = await command.searchPlaylists({ search, token: servers[0].accessToken }) | ||
207 | expect(body.total).to.equal(0) | ||
208 | expect(body.data).to.have.lengthOf(0) | ||
209 | }) | ||
210 | |||
211 | after(async function () { | ||
212 | await cleanupTests(servers) | ||
213 | }) | ||
214 | }) | ||
diff --git a/packages/tests/src/api/search/search-activitypub-videos.ts b/packages/tests/src/api/search/search-activitypub-videos.ts new file mode 100644 index 000000000..72759f21e --- /dev/null +++ b/packages/tests/src/api/search/search-activitypub-videos.ts | |||
@@ -0,0 +1,196 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test ActivityPub videos search', function () { | ||
18 | let servers: PeerTubeServer[] | ||
19 | let videoServer1UUID: string | ||
20 | let videoServer2UUID: string | ||
21 | |||
22 | let command: SearchCommand | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(120000) | ||
26 | |||
27 | servers = await createMultipleServers(2) | ||
28 | |||
29 | await setAccessTokensToServers(servers) | ||
30 | await setDefaultVideoChannel(servers) | ||
31 | await setDefaultAccountAvatar(servers) | ||
32 | |||
33 | { | ||
34 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1 on server 1' } }) | ||
35 | videoServer1UUID = uuid | ||
36 | } | ||
37 | |||
38 | { | ||
39 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 1 on server 2' } }) | ||
40 | videoServer2UUID = uuid | ||
41 | } | ||
42 | |||
43 | await waitJobs(servers) | ||
44 | |||
45 | command = servers[0].search | ||
46 | }) | ||
47 | |||
48 | it('Should not find a remote video', async function () { | ||
49 | { | ||
50 | const search = servers[1].url + '/videos/watch/43' | ||
51 | const body = await command.searchVideos({ search, token: servers[0].accessToken }) | ||
52 | |||
53 | expect(body.total).to.equal(0) | ||
54 | expect(body.data).to.be.an('array') | ||
55 | expect(body.data).to.have.lengthOf(0) | ||
56 | } | ||
57 | |||
58 | { | ||
59 | // Without token | ||
60 | const search = servers[1].url + '/videos/watch/' + videoServer2UUID | ||
61 | const body = await command.searchVideos({ search }) | ||
62 | |||
63 | expect(body.total).to.equal(0) | ||
64 | expect(body.data).to.be.an('array') | ||
65 | expect(body.data).to.have.lengthOf(0) | ||
66 | } | ||
67 | }) | ||
68 | |||
69 | it('Should search a local video', async function () { | ||
70 | const search = servers[0].url + '/videos/watch/' + videoServer1UUID | ||
71 | const body = await command.searchVideos({ search }) | ||
72 | |||
73 | expect(body.total).to.equal(1) | ||
74 | expect(body.data).to.be.an('array') | ||
75 | expect(body.data).to.have.lengthOf(1) | ||
76 | expect(body.data[0].name).to.equal('video 1 on server 1') | ||
77 | }) | ||
78 | |||
79 | it('Should search a local video with an alternative URL', async function () { | ||
80 | const search = servers[0].url + '/w/' + videoServer1UUID | ||
81 | const body1 = await command.searchVideos({ search }) | ||
82 | const body2 = await command.searchVideos({ search, token: servers[0].accessToken }) | ||
83 | |||
84 | for (const body of [ body1, body2 ]) { | ||
85 | expect(body.total).to.equal(1) | ||
86 | expect(body.data).to.be.an('array') | ||
87 | expect(body.data).to.have.lengthOf(1) | ||
88 | expect(body.data[0].name).to.equal('video 1 on server 1') | ||
89 | } | ||
90 | }) | ||
91 | |||
92 | it('Should search a local video with a query in URL', async function () { | ||
93 | const searches = [ | ||
94 | servers[0].url + '/w/' + videoServer1UUID, | ||
95 | servers[0].url + '/videos/watch/' + videoServer1UUID | ||
96 | ] | ||
97 | |||
98 | for (const search of searches) { | ||
99 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
100 | const body = await command.searchVideos({ search: search + '?startTime=4', token }) | ||
101 | |||
102 | expect(body.total).to.equal(1) | ||
103 | expect(body.data).to.be.an('array') | ||
104 | expect(body.data).to.have.lengthOf(1) | ||
105 | expect(body.data[0].name).to.equal('video 1 on server 1') | ||
106 | } | ||
107 | } | ||
108 | }) | ||
109 | |||
110 | it('Should search a remote video', async function () { | ||
111 | const searches = [ | ||
112 | servers[1].url + '/w/' + videoServer2UUID, | ||
113 | servers[1].url + '/videos/watch/' + videoServer2UUID | ||
114 | ] | ||
115 | |||
116 | for (const search of searches) { | ||
117 | const body = await command.searchVideos({ search, token: servers[0].accessToken }) | ||
118 | |||
119 | expect(body.total).to.equal(1) | ||
120 | expect(body.data).to.be.an('array') | ||
121 | expect(body.data).to.have.lengthOf(1) | ||
122 | expect(body.data[0].name).to.equal('video 1 on server 2') | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | it('Should not list this remote video', async function () { | ||
127 | const { total, data } = await servers[0].videos.list() | ||
128 | expect(total).to.equal(1) | ||
129 | expect(data).to.have.lengthOf(1) | ||
130 | expect(data[0].name).to.equal('video 1 on server 1') | ||
131 | }) | ||
132 | |||
133 | it('Should update video of server 2, and refresh it on server 1', async function () { | ||
134 | this.timeout(120000) | ||
135 | |||
136 | const channelAttributes = { | ||
137 | name: 'super_channel', | ||
138 | displayName: 'super channel' | ||
139 | } | ||
140 | const created = await servers[1].channels.create({ attributes: channelAttributes }) | ||
141 | const videoChannelId = created.id | ||
142 | |||
143 | const attributes = { | ||
144 | name: 'updated', | ||
145 | tag: [ 'tag1', 'tag2' ], | ||
146 | privacy: VideoPrivacy.UNLISTED, | ||
147 | channelId: videoChannelId | ||
148 | } | ||
149 | await servers[1].videos.update({ id: videoServer2UUID, attributes }) | ||
150 | |||
151 | await waitJobs(servers) | ||
152 | // Expire video | ||
153 | await wait(10000) | ||
154 | |||
155 | // Will run refresh async | ||
156 | const search = servers[1].url + '/videos/watch/' + videoServer2UUID | ||
157 | await command.searchVideos({ search, token: servers[0].accessToken }) | ||
158 | |||
159 | // Wait refresh | ||
160 | await wait(5000) | ||
161 | |||
162 | const body = await command.searchVideos({ search, token: servers[0].accessToken }) | ||
163 | expect(body.total).to.equal(1) | ||
164 | expect(body.data).to.have.lengthOf(1) | ||
165 | |||
166 | const video = body.data[0] | ||
167 | expect(video.name).to.equal('updated') | ||
168 | expect(video.channel.name).to.equal('super_channel') | ||
169 | expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED) | ||
170 | }) | ||
171 | |||
172 | it('Should delete video of server 2, and delete it on server 1', async function () { | ||
173 | this.timeout(120000) | ||
174 | |||
175 | await servers[1].videos.remove({ id: videoServer2UUID }) | ||
176 | |||
177 | await waitJobs(servers) | ||
178 | // Expire video | ||
179 | await wait(10000) | ||
180 | |||
181 | // Will run refresh async | ||
182 | const search = servers[1].url + '/videos/watch/' + videoServer2UUID | ||
183 | await command.searchVideos({ search, token: servers[0].accessToken }) | ||
184 | |||
185 | // Wait refresh | ||
186 | await wait(5000) | ||
187 | |||
188 | const body = await command.searchVideos({ search, token: servers[0].accessToken }) | ||
189 | expect(body.total).to.equal(0) | ||
190 | expect(body.data).to.have.lengthOf(0) | ||
191 | }) | ||
192 | |||
193 | after(async function () { | ||
194 | await cleanupTests(servers) | ||
195 | }) | ||
196 | }) | ||
diff --git a/packages/tests/src/api/search/search-channels.ts b/packages/tests/src/api/search/search-channels.ts new file mode 100644 index 000000000..36596e036 --- /dev/null +++ b/packages/tests/src/api/search/search-channels.ts | |||
@@ -0,0 +1,159 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoChannel } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Test channels search', function () { | ||
17 | let server: PeerTubeServer | ||
18 | let remoteServer: PeerTubeServer | ||
19 | let command: SearchCommand | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(120000) | ||
23 | |||
24 | const servers = await Promise.all([ | ||
25 | createSingleServer(1), | ||
26 | createSingleServer(2) | ||
27 | ]) | ||
28 | server = servers[0] | ||
29 | remoteServer = servers[1] | ||
30 | |||
31 | await setAccessTokensToServers([ server, remoteServer ]) | ||
32 | await setDefaultChannelAvatar(server) | ||
33 | await setDefaultAccountAvatar(server) | ||
34 | |||
35 | await servers[1].config.disableTranscoding() | ||
36 | |||
37 | { | ||
38 | await server.users.create({ username: 'user1' }) | ||
39 | const channel = { | ||
40 | name: 'squall_channel', | ||
41 | displayName: 'Squall channel' | ||
42 | } | ||
43 | await server.channels.create({ attributes: channel }) | ||
44 | } | ||
45 | |||
46 | { | ||
47 | await remoteServer.users.create({ username: 'user1' }) | ||
48 | const channel = { | ||
49 | name: 'zell_channel', | ||
50 | displayName: 'Zell channel' | ||
51 | } | ||
52 | const { id } = await remoteServer.channels.create({ attributes: channel }) | ||
53 | |||
54 | await remoteServer.videos.upload({ attributes: { channelId: id } }) | ||
55 | } | ||
56 | |||
57 | await doubleFollow(server, remoteServer) | ||
58 | |||
59 | command = server.search | ||
60 | }) | ||
61 | |||
62 | it('Should make a simple search and not have results', async function () { | ||
63 | const body = await command.searchChannels({ search: 'abc' }) | ||
64 | |||
65 | expect(body.total).to.equal(0) | ||
66 | expect(body.data).to.have.lengthOf(0) | ||
67 | }) | ||
68 | |||
69 | it('Should make a search and have results', async function () { | ||
70 | { | ||
71 | const search = { | ||
72 | search: 'Squall', | ||
73 | start: 0, | ||
74 | count: 1 | ||
75 | } | ||
76 | const body = await command.advancedChannelSearch({ search }) | ||
77 | expect(body.total).to.equal(1) | ||
78 | expect(body.data).to.have.lengthOf(1) | ||
79 | |||
80 | const channel: VideoChannel = body.data[0] | ||
81 | expect(channel.name).to.equal('squall_channel') | ||
82 | expect(channel.displayName).to.equal('Squall channel') | ||
83 | } | ||
84 | |||
85 | { | ||
86 | const search = { | ||
87 | search: 'Squall', | ||
88 | start: 1, | ||
89 | count: 1 | ||
90 | } | ||
91 | |||
92 | const body = await command.advancedChannelSearch({ search }) | ||
93 | expect(body.total).to.equal(1) | ||
94 | expect(body.data).to.have.lengthOf(0) | ||
95 | } | ||
96 | }) | ||
97 | |||
98 | it('Should filter by host', async function () { | ||
99 | { | ||
100 | const search = { search: 'channel', host: remoteServer.host } | ||
101 | |||
102 | const body = await command.advancedChannelSearch({ search }) | ||
103 | expect(body.total).to.equal(1) | ||
104 | expect(body.data).to.have.lengthOf(1) | ||
105 | expect(body.data[0].displayName).to.equal('Zell channel') | ||
106 | } | ||
107 | |||
108 | { | ||
109 | const search = { search: 'Sq', host: server.host } | ||
110 | |||
111 | const body = await command.advancedChannelSearch({ search }) | ||
112 | expect(body.total).to.equal(1) | ||
113 | expect(body.data).to.have.lengthOf(1) | ||
114 | expect(body.data[0].displayName).to.equal('Squall channel') | ||
115 | } | ||
116 | |||
117 | { | ||
118 | const search = { search: 'Squall', host: 'example.com' } | ||
119 | |||
120 | const body = await command.advancedChannelSearch({ search }) | ||
121 | expect(body.total).to.equal(0) | ||
122 | expect(body.data).to.have.lengthOf(0) | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | it('Should filter by names', async function () { | ||
127 | { | ||
128 | const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel', 'zell_channel' ] } }) | ||
129 | expect(body.total).to.equal(1) | ||
130 | expect(body.data).to.have.lengthOf(1) | ||
131 | expect(body.data[0].displayName).to.equal('Squall channel') | ||
132 | } | ||
133 | |||
134 | { | ||
135 | const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel@' + server.host ] } }) | ||
136 | expect(body.total).to.equal(1) | ||
137 | expect(body.data).to.have.lengthOf(1) | ||
138 | expect(body.data[0].displayName).to.equal('Squall channel') | ||
139 | } | ||
140 | |||
141 | { | ||
142 | const body = await command.advancedChannelSearch({ search: { handles: [ 'chocobozzz_channel' ] } }) | ||
143 | expect(body.total).to.equal(0) | ||
144 | expect(body.data).to.have.lengthOf(0) | ||
145 | } | ||
146 | |||
147 | { | ||
148 | const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel', 'zell_channel@' + remoteServer.host ] } }) | ||
149 | expect(body.total).to.equal(2) | ||
150 | expect(body.data).to.have.lengthOf(2) | ||
151 | expect(body.data[0].displayName).to.equal('Squall channel') | ||
152 | expect(body.data[1].displayName).to.equal('Zell channel') | ||
153 | } | ||
154 | }) | ||
155 | |||
156 | after(async function () { | ||
157 | await cleanupTests([ server, remoteServer ]) | ||
158 | }) | ||
159 | }) | ||
diff --git a/packages/tests/src/api/search/search-index.ts b/packages/tests/src/api/search/search-index.ts new file mode 100644 index 000000000..4bac7ea94 --- /dev/null +++ b/packages/tests/src/api/search/search-index.ts | |||
@@ -0,0 +1,438 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | BooleanBothQuery, | ||
6 | VideoChannelsSearchQuery, | ||
7 | VideoPlaylistPrivacy, | ||
8 | VideoPlaylistsSearchQuery, | ||
9 | VideoPlaylistType, | ||
10 | VideosSearchQuery | ||
11 | } from '@peertube/peertube-models' | ||
12 | import { | ||
13 | cleanupTests, | ||
14 | createSingleServer, | ||
15 | PeerTubeServer, | ||
16 | SearchCommand, | ||
17 | setAccessTokensToServers | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | |||
20 | describe('Test index search', function () { | ||
21 | const localVideoName = 'local video' + new Date().toISOString() | ||
22 | |||
23 | let server: PeerTubeServer = null | ||
24 | let command: SearchCommand | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(30000) | ||
28 | |||
29 | server = await createSingleServer(1) | ||
30 | |||
31 | await setAccessTokensToServers([ server ]) | ||
32 | |||
33 | await server.videos.upload({ attributes: { name: localVideoName } }) | ||
34 | |||
35 | command = server.search | ||
36 | }) | ||
37 | |||
38 | describe('Default search', async function () { | ||
39 | |||
40 | it('Should make a local videos search by default', async function () { | ||
41 | await server.config.updateCustomSubConfig({ | ||
42 | newConfig: { | ||
43 | search: { | ||
44 | searchIndex: { | ||
45 | enabled: true, | ||
46 | isDefaultSearch: false, | ||
47 | disableLocalSearch: false | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | }) | ||
52 | |||
53 | const body = await command.searchVideos({ search: 'local video' }) | ||
54 | |||
55 | expect(body.total).to.equal(1) | ||
56 | expect(body.data[0].name).to.equal(localVideoName) | ||
57 | }) | ||
58 | |||
59 | it('Should make a local channels search by default', async function () { | ||
60 | const body = await command.searchChannels({ search: 'root' }) | ||
61 | |||
62 | expect(body.total).to.equal(1) | ||
63 | expect(body.data[0].name).to.equal('root_channel') | ||
64 | expect(body.data[0].host).to.equal(server.host) | ||
65 | }) | ||
66 | |||
67 | it('Should make an index videos search by default', async function () { | ||
68 | await server.config.updateCustomSubConfig({ | ||
69 | newConfig: { | ||
70 | search: { | ||
71 | searchIndex: { | ||
72 | enabled: true, | ||
73 | isDefaultSearch: true, | ||
74 | disableLocalSearch: false | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | const body = await command.searchVideos({ search: 'local video' }) | ||
81 | expect(body.total).to.be.greaterThan(2) | ||
82 | }) | ||
83 | |||
84 | it('Should make an index channels search by default', async function () { | ||
85 | const body = await command.searchChannels({ search: 'root' }) | ||
86 | expect(body.total).to.be.greaterThan(2) | ||
87 | }) | ||
88 | }) | ||
89 | |||
90 | describe('Videos search', async function () { | ||
91 | |||
92 | async function check (search: VideosSearchQuery, exists = true) { | ||
93 | const body = await command.advancedVideoSearch({ search }) | ||
94 | |||
95 | if (exists === false) { | ||
96 | expect(body.total).to.equal(0) | ||
97 | expect(body.data).to.have.lengthOf(0) | ||
98 | return | ||
99 | } | ||
100 | |||
101 | expect(body.total).to.equal(1) | ||
102 | expect(body.data).to.have.lengthOf(1) | ||
103 | |||
104 | const video = body.data[0] | ||
105 | |||
106 | expect(video.name).to.equal('What is PeerTube?') | ||
107 | expect(video.category.label).to.equal('Science & Technology') | ||
108 | expect(video.licence.label).to.equal('Attribution - Share Alike') | ||
109 | expect(video.privacy.label).to.equal('Public') | ||
110 | expect(video.duration).to.equal(113) | ||
111 | expect(video.thumbnailUrl.startsWith('https://framatube.org/static/thumbnails')).to.be.true | ||
112 | |||
113 | expect(video.account.host).to.equal('framatube.org') | ||
114 | expect(video.account.name).to.equal('framasoft') | ||
115 | expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft') | ||
116 | expect(video.account.avatars.length).to.equal(2, 'Account should have one avatar image') | ||
117 | |||
118 | expect(video.channel.host).to.equal('framatube.org') | ||
119 | expect(video.channel.name).to.equal('joinpeertube') | ||
120 | expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube') | ||
121 | expect(video.channel.avatars.length).to.equal(2, 'Channel should have one avatar image') | ||
122 | } | ||
123 | |||
124 | const baseSearch: VideosSearchQuery = { | ||
125 | search: 'what is peertube', | ||
126 | start: 0, | ||
127 | count: 2, | ||
128 | categoryOneOf: [ 15 ], | ||
129 | licenceOneOf: [ 2 ], | ||
130 | tagsAllOf: [ 'framasoft', 'peertube' ], | ||
131 | startDate: '2018-10-01T10:50:46.396Z', | ||
132 | endDate: '2018-10-01T10:55:46.396Z' | ||
133 | } | ||
134 | |||
135 | it('Should make a simple search and not have results', async function () { | ||
136 | const body = await command.searchVideos({ search: 'djidane'.repeat(50) }) | ||
137 | |||
138 | expect(body.total).to.equal(0) | ||
139 | expect(body.data).to.have.lengthOf(0) | ||
140 | }) | ||
141 | |||
142 | it('Should make a simple search and have results', async function () { | ||
143 | const body = await command.searchVideos({ search: 'What is PeerTube' }) | ||
144 | |||
145 | expect(body.total).to.be.greaterThan(1) | ||
146 | }) | ||
147 | |||
148 | it('Should make a simple search', async function () { | ||
149 | await check(baseSearch) | ||
150 | }) | ||
151 | |||
152 | it('Should search by start date', async function () { | ||
153 | const search = { ...baseSearch, startDate: '2018-10-01T10:54:46.396Z' } | ||
154 | await check(search, false) | ||
155 | }) | ||
156 | |||
157 | it('Should search by tags', async function () { | ||
158 | const search = { ...baseSearch, tagsAllOf: [ 'toto', 'framasoft' ] } | ||
159 | await check(search, false) | ||
160 | }) | ||
161 | |||
162 | it('Should search by duration', async function () { | ||
163 | const search = { ...baseSearch, durationMin: 2000 } | ||
164 | await check(search, false) | ||
165 | }) | ||
166 | |||
167 | it('Should search by nsfw attribute', async function () { | ||
168 | { | ||
169 | const search = { ...baseSearch, nsfw: 'true' as BooleanBothQuery } | ||
170 | await check(search, false) | ||
171 | } | ||
172 | |||
173 | { | ||
174 | const search = { ...baseSearch, nsfw: 'false' as BooleanBothQuery } | ||
175 | await check(search, true) | ||
176 | } | ||
177 | |||
178 | { | ||
179 | const search = { ...baseSearch, nsfw: 'both' as BooleanBothQuery } | ||
180 | await check(search, true) | ||
181 | } | ||
182 | }) | ||
183 | |||
184 | it('Should search by host', async function () { | ||
185 | { | ||
186 | const search = { ...baseSearch, host: 'example.com' } | ||
187 | await check(search, false) | ||
188 | } | ||
189 | |||
190 | { | ||
191 | const search = { ...baseSearch, host: 'framatube.org' } | ||
192 | await check(search, true) | ||
193 | } | ||
194 | }) | ||
195 | |||
196 | it('Should search by uuids', async function () { | ||
197 | const goodUUID = '9c9de5e8-0a1e-484a-b099-e80766180a6d' | ||
198 | const goodShortUUID = 'kkGMgK9ZtnKfYAgnEtQxbv' | ||
199 | const badUUID = 'c29c5b77-4a04-493d-96a9-2e9267e308f0' | ||
200 | const badShortUUID = 'rP5RgUeX9XwTSrspCdkDej' | ||
201 | |||
202 | { | ||
203 | const uuidsMatrix = [ | ||
204 | [ goodUUID ], | ||
205 | [ goodUUID, badShortUUID ], | ||
206 | [ badShortUUID, goodShortUUID ], | ||
207 | [ goodUUID, goodShortUUID ] | ||
208 | ] | ||
209 | |||
210 | for (const uuids of uuidsMatrix) { | ||
211 | const search = { ...baseSearch, uuids } | ||
212 | await check(search, true) | ||
213 | } | ||
214 | } | ||
215 | |||
216 | { | ||
217 | const uuidsMatrix = [ | ||
218 | [ badUUID ], | ||
219 | [ badShortUUID ] | ||
220 | ] | ||
221 | |||
222 | for (const uuids of uuidsMatrix) { | ||
223 | const search = { ...baseSearch, uuids } | ||
224 | await check(search, false) | ||
225 | } | ||
226 | } | ||
227 | }) | ||
228 | |||
229 | it('Should have a correct pagination', async function () { | ||
230 | const search = { | ||
231 | search: 'video', | ||
232 | start: 0, | ||
233 | count: 5 | ||
234 | } | ||
235 | |||
236 | const body = await command.advancedVideoSearch({ search }) | ||
237 | |||
238 | expect(body.total).to.be.greaterThan(5) | ||
239 | expect(body.data).to.have.lengthOf(5) | ||
240 | }) | ||
241 | |||
242 | it('Should use the nsfw instance policy as default', async function () { | ||
243 | let nsfwUUID: string | ||
244 | |||
245 | { | ||
246 | await server.config.updateCustomSubConfig({ | ||
247 | newConfig: { | ||
248 | instance: { defaultNSFWPolicy: 'display' } | ||
249 | } | ||
250 | }) | ||
251 | |||
252 | const body = await command.searchVideos({ search: 'NSFW search index', sort: '-match' }) | ||
253 | expect(body.data).to.have.length.greaterThan(0) | ||
254 | |||
255 | const video = body.data[0] | ||
256 | expect(video.nsfw).to.be.true | ||
257 | |||
258 | nsfwUUID = video.uuid | ||
259 | } | ||
260 | |||
261 | { | ||
262 | await server.config.updateCustomSubConfig({ | ||
263 | newConfig: { | ||
264 | instance: { defaultNSFWPolicy: 'do_not_list' } | ||
265 | } | ||
266 | }) | ||
267 | |||
268 | const body = await command.searchVideos({ search: 'NSFW search index', sort: '-match' }) | ||
269 | |||
270 | try { | ||
271 | expect(body.data).to.have.lengthOf(0) | ||
272 | } catch { | ||
273 | const video = body.data[0] | ||
274 | |||
275 | expect(video.uuid).not.equal(nsfwUUID) | ||
276 | } | ||
277 | } | ||
278 | }) | ||
279 | }) | ||
280 | |||
281 | describe('Channels search', async function () { | ||
282 | |||
283 | async function check (search: VideoChannelsSearchQuery, exists = true) { | ||
284 | const body = await command.advancedChannelSearch({ search }) | ||
285 | |||
286 | if (exists === false) { | ||
287 | expect(body.total).to.equal(0) | ||
288 | expect(body.data).to.have.lengthOf(0) | ||
289 | return | ||
290 | } | ||
291 | |||
292 | expect(body.total).to.be.greaterThan(0) | ||
293 | expect(body.data).to.have.length.greaterThan(0) | ||
294 | |||
295 | const videoChannel = body.data[0] | ||
296 | expect(videoChannel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8') | ||
297 | expect(videoChannel.host).to.equal('framatube.org') | ||
298 | expect(videoChannel.avatars.length).to.equal(2, 'Channel should have two avatar images') | ||
299 | expect(videoChannel.displayName).to.exist | ||
300 | |||
301 | expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft') | ||
302 | expect(videoChannel.ownerAccount.name).to.equal('framasoft') | ||
303 | expect(videoChannel.ownerAccount.host).to.equal('framatube.org') | ||
304 | expect(videoChannel.ownerAccount.avatars.length).to.equal(2, 'Account should have two avatar images') | ||
305 | } | ||
306 | |||
307 | it('Should make a simple search and not have results', async function () { | ||
308 | const body = await command.searchChannels({ search: 'a'.repeat(500) }) | ||
309 | |||
310 | expect(body.total).to.equal(0) | ||
311 | expect(body.data).to.have.lengthOf(0) | ||
312 | }) | ||
313 | |||
314 | it('Should make a search and have results', async function () { | ||
315 | await check({ search: 'Framasoft', sort: 'createdAt' }, true) | ||
316 | }) | ||
317 | |||
318 | it('Should make host search and have appropriate results', async function () { | ||
319 | await check({ search: 'Framasoft videos', host: 'example.com' }, false) | ||
320 | await check({ search: 'Framasoft videos', host: 'framatube.org' }, true) | ||
321 | }) | ||
322 | |||
323 | it('Should make handles search and have appropriate results', async function () { | ||
324 | await check({ handles: [ 'bf54d359-cfad-4935-9d45-9d6be93f63e8@framatube.org' ] }, true) | ||
325 | await check({ handles: [ 'jeanine', 'bf54d359-cfad-4935-9d45-9d6be93f63e8@framatube.org' ] }, true) | ||
326 | await check({ handles: [ 'jeanine', 'chocobozzz_channel2@peertube2.cpy.re' ] }, false) | ||
327 | }) | ||
328 | |||
329 | it('Should have a correct pagination', async function () { | ||
330 | const body = await command.advancedChannelSearch({ search: { search: 'root', start: 0, count: 2 } }) | ||
331 | |||
332 | expect(body.total).to.be.greaterThan(2) | ||
333 | expect(body.data).to.have.lengthOf(2) | ||
334 | }) | ||
335 | }) | ||
336 | |||
337 | describe('Playlists search', async function () { | ||
338 | |||
339 | async function check (search: VideoPlaylistsSearchQuery, exists = true) { | ||
340 | const body = await command.advancedPlaylistSearch({ search }) | ||
341 | |||
342 | if (exists === false) { | ||
343 | expect(body.total).to.equal(0) | ||
344 | expect(body.data).to.have.lengthOf(0) | ||
345 | return | ||
346 | } | ||
347 | |||
348 | expect(body.total).to.be.greaterThan(0) | ||
349 | expect(body.data).to.have.length.greaterThan(0) | ||
350 | |||
351 | const videoPlaylist = body.data[0] | ||
352 | |||
353 | expect(videoPlaylist.url).to.equal('https://peertube2.cpy.re/videos/watch/playlist/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
354 | expect(videoPlaylist.thumbnailUrl).to.exist | ||
355 | expect(videoPlaylist.embedUrl).to.equal('https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
356 | |||
357 | expect(videoPlaylist.type.id).to.equal(VideoPlaylistType.REGULAR) | ||
358 | expect(videoPlaylist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) | ||
359 | expect(videoPlaylist.videosLength).to.exist | ||
360 | |||
361 | expect(videoPlaylist.createdAt).to.exist | ||
362 | expect(videoPlaylist.updatedAt).to.exist | ||
363 | |||
364 | expect(videoPlaylist.uuid).to.equal('73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
365 | expect(videoPlaylist.displayName).to.exist | ||
366 | |||
367 | expect(videoPlaylist.ownerAccount.url).to.equal('https://peertube2.cpy.re/accounts/chocobozzz') | ||
368 | expect(videoPlaylist.ownerAccount.name).to.equal('chocobozzz') | ||
369 | expect(videoPlaylist.ownerAccount.host).to.equal('peertube2.cpy.re') | ||
370 | expect(videoPlaylist.ownerAccount.avatars.length).to.equal(2, 'Account should have two avatar images') | ||
371 | |||
372 | expect(videoPlaylist.videoChannel.url).to.equal('https://peertube2.cpy.re/video-channels/chocobozzz_channel') | ||
373 | expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel') | ||
374 | expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re') | ||
375 | expect(videoPlaylist.videoChannel.avatars.length).to.equal(2, 'Channel should have two avatar images') | ||
376 | } | ||
377 | |||
378 | it('Should make a simple search and not have results', async function () { | ||
379 | const body = await command.searchPlaylists({ search: 'a'.repeat(500) }) | ||
380 | |||
381 | expect(body.total).to.equal(0) | ||
382 | expect(body.data).to.have.lengthOf(0) | ||
383 | }) | ||
384 | |||
385 | it('Should make a search and have results', async function () { | ||
386 | await check({ search: 'E2E playlist', sort: '-match' }, true) | ||
387 | }) | ||
388 | |||
389 | it('Should make host search and have appropriate results', async function () { | ||
390 | await check({ search: 'E2E playlist', host: 'example.com' }, false) | ||
391 | await check({ search: 'E2E playlist', host: 'peertube2.cpy.re', sort: '-match' }, true) | ||
392 | }) | ||
393 | |||
394 | it('Should make a search by uuids and have appropriate results', async function () { | ||
395 | const goodUUID = '73804a40-da9a-40c2-b1eb-2c6d9eec8f0a' | ||
396 | const goodShortUUID = 'fgei1ws1oa6FCaJ2qZPG29' | ||
397 | const badUUID = 'c29c5b77-4a04-493d-96a9-2e9267e308f0' | ||
398 | const badShortUUID = 'rP5RgUeX9XwTSrspCdkDej' | ||
399 | |||
400 | { | ||
401 | const uuidsMatrix = [ | ||
402 | [ goodUUID ], | ||
403 | [ goodUUID, badShortUUID ], | ||
404 | [ badShortUUID, goodShortUUID ], | ||
405 | [ goodUUID, goodShortUUID ] | ||
406 | ] | ||
407 | |||
408 | for (const uuids of uuidsMatrix) { | ||
409 | const search = { search: 'E2E playlist', sort: '-match', uuids } | ||
410 | await check(search, true) | ||
411 | } | ||
412 | } | ||
413 | |||
414 | { | ||
415 | const uuidsMatrix = [ | ||
416 | [ badUUID ], | ||
417 | [ badShortUUID ] | ||
418 | ] | ||
419 | |||
420 | for (const uuids of uuidsMatrix) { | ||
421 | const search = { search: 'E2E playlist', sort: '-match', uuids } | ||
422 | await check(search, false) | ||
423 | } | ||
424 | } | ||
425 | }) | ||
426 | |||
427 | it('Should have a correct pagination', async function () { | ||
428 | const body = await command.advancedChannelSearch({ search: { search: 'root', start: 0, count: 2 } }) | ||
429 | |||
430 | expect(body.total).to.be.greaterThan(2) | ||
431 | expect(body.data).to.have.lengthOf(2) | ||
432 | }) | ||
433 | }) | ||
434 | |||
435 | after(async function () { | ||
436 | await cleanupTests([ server ]) | ||
437 | }) | ||
438 | }) | ||
diff --git a/packages/tests/src/api/search/search-playlists.ts b/packages/tests/src/api/search/search-playlists.ts new file mode 100644 index 000000000..cd16e202e --- /dev/null +++ b/packages/tests/src/api/search/search-playlists.ts | |||
@@ -0,0 +1,180 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar, | ||
14 | setDefaultVideoChannel | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test playlists search', function () { | ||
18 | let server: PeerTubeServer | ||
19 | let remoteServer: PeerTubeServer | ||
20 | let command: SearchCommand | ||
21 | let playlistUUID: string | ||
22 | let playlistShortUUID: string | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(120000) | ||
26 | |||
27 | const servers = await Promise.all([ | ||
28 | createSingleServer(1), | ||
29 | createSingleServer(2) | ||
30 | ]) | ||
31 | server = servers[0] | ||
32 | remoteServer = servers[1] | ||
33 | |||
34 | await setAccessTokensToServers([ remoteServer, server ]) | ||
35 | await setDefaultVideoChannel([ remoteServer, server ]) | ||
36 | await setDefaultChannelAvatar([ remoteServer, server ]) | ||
37 | await setDefaultAccountAvatar([ remoteServer, server ]) | ||
38 | |||
39 | await servers[1].config.disableTranscoding() | ||
40 | |||
41 | { | ||
42 | const videoId = (await server.videos.upload()).uuid | ||
43 | |||
44 | const attributes = { | ||
45 | displayName: 'Dr. Kenzo Tenma hospital videos', | ||
46 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
47 | videoChannelId: server.store.channel.id | ||
48 | } | ||
49 | const created = await server.playlists.create({ attributes }) | ||
50 | playlistUUID = created.uuid | ||
51 | playlistShortUUID = created.shortUUID | ||
52 | |||
53 | await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } }) | ||
54 | } | ||
55 | |||
56 | { | ||
57 | const videoId = (await remoteServer.videos.upload()).uuid | ||
58 | |||
59 | const attributes = { | ||
60 | displayName: 'Johan & Anna Libert music videos', | ||
61 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
62 | videoChannelId: remoteServer.store.channel.id | ||
63 | } | ||
64 | const created = await remoteServer.playlists.create({ attributes }) | ||
65 | |||
66 | await remoteServer.playlists.addElement({ playlistId: created.id, attributes: { videoId } }) | ||
67 | } | ||
68 | |||
69 | { | ||
70 | const attributes = { | ||
71 | displayName: 'Inspector Lunge playlist', | ||
72 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
73 | videoChannelId: server.store.channel.id | ||
74 | } | ||
75 | await server.playlists.create({ attributes }) | ||
76 | } | ||
77 | |||
78 | await doubleFollow(server, remoteServer) | ||
79 | |||
80 | command = server.search | ||
81 | }) | ||
82 | |||
83 | it('Should make a simple search and not have results', async function () { | ||
84 | const body = await command.searchPlaylists({ search: 'abc' }) | ||
85 | |||
86 | expect(body.total).to.equal(0) | ||
87 | expect(body.data).to.have.lengthOf(0) | ||
88 | }) | ||
89 | |||
90 | it('Should make a search and have results', async function () { | ||
91 | { | ||
92 | const search = { | ||
93 | search: 'tenma', | ||
94 | start: 0, | ||
95 | count: 1 | ||
96 | } | ||
97 | const body = await command.advancedPlaylistSearch({ search }) | ||
98 | expect(body.total).to.equal(1) | ||
99 | expect(body.data).to.have.lengthOf(1) | ||
100 | |||
101 | const playlist = body.data[0] | ||
102 | expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos') | ||
103 | expect(playlist.url).to.equal(server.url + '/video-playlists/' + playlist.uuid) | ||
104 | } | ||
105 | |||
106 | { | ||
107 | const search = { | ||
108 | search: 'Anna Livert music', | ||
109 | start: 0, | ||
110 | count: 1 | ||
111 | } | ||
112 | const body = await command.advancedPlaylistSearch({ search }) | ||
113 | expect(body.total).to.equal(1) | ||
114 | expect(body.data).to.have.lengthOf(1) | ||
115 | |||
116 | const playlist = body.data[0] | ||
117 | expect(playlist.displayName).to.equal('Johan & Anna Libert music videos') | ||
118 | } | ||
119 | }) | ||
120 | |||
121 | it('Should filter by host', async function () { | ||
122 | { | ||
123 | const search = { search: 'tenma', host: server.host } | ||
124 | const body = await command.advancedPlaylistSearch({ search }) | ||
125 | expect(body.total).to.equal(1) | ||
126 | expect(body.data).to.have.lengthOf(1) | ||
127 | |||
128 | const playlist = body.data[0] | ||
129 | expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos') | ||
130 | } | ||
131 | |||
132 | { | ||
133 | const search = { search: 'Anna', host: 'example.com' } | ||
134 | const body = await command.advancedPlaylistSearch({ search }) | ||
135 | expect(body.total).to.equal(0) | ||
136 | expect(body.data).to.have.lengthOf(0) | ||
137 | } | ||
138 | |||
139 | { | ||
140 | const search = { search: 'video', host: remoteServer.host } | ||
141 | const body = await command.advancedPlaylistSearch({ search }) | ||
142 | expect(body.total).to.equal(1) | ||
143 | expect(body.data).to.have.lengthOf(1) | ||
144 | |||
145 | const playlist = body.data[0] | ||
146 | expect(playlist.displayName).to.equal('Johan & Anna Libert music videos') | ||
147 | } | ||
148 | }) | ||
149 | |||
150 | it('Should filter by UUIDs', async function () { | ||
151 | for (const uuid of [ playlistUUID, playlistShortUUID ]) { | ||
152 | const body = await command.advancedPlaylistSearch({ search: { uuids: [ uuid ] } }) | ||
153 | |||
154 | expect(body.total).to.equal(1) | ||
155 | expect(body.data[0].displayName).to.equal('Dr. Kenzo Tenma hospital videos') | ||
156 | } | ||
157 | |||
158 | { | ||
159 | const body = await command.advancedPlaylistSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } }) | ||
160 | |||
161 | expect(body.total).to.equal(0) | ||
162 | expect(body.data).to.have.lengthOf(0) | ||
163 | } | ||
164 | }) | ||
165 | |||
166 | it('Should not display playlists without videos', async function () { | ||
167 | const search = { | ||
168 | search: 'Lunge', | ||
169 | start: 0, | ||
170 | count: 1 | ||
171 | } | ||
172 | const body = await command.advancedPlaylistSearch({ search }) | ||
173 | expect(body.total).to.equal(0) | ||
174 | expect(body.data).to.have.lengthOf(0) | ||
175 | }) | ||
176 | |||
177 | after(async function () { | ||
178 | await cleanupTests([ server, remoteServer ]) | ||
179 | }) | ||
180 | }) | ||
diff --git a/packages/tests/src/api/search/search-videos.ts b/packages/tests/src/api/search/search-videos.ts new file mode 100644 index 000000000..0739f0886 --- /dev/null +++ b/packages/tests/src/api/search/search-videos.ts | |||
@@ -0,0 +1,568 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | SearchCommand, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar, | ||
15 | setDefaultVideoChannel, | ||
16 | stopFfmpeg | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | |||
19 | describe('Test videos search', function () { | ||
20 | let server: PeerTubeServer | ||
21 | let remoteServer: PeerTubeServer | ||
22 | let startDate: string | ||
23 | let videoUUID: string | ||
24 | let videoShortUUID: string | ||
25 | |||
26 | let command: SearchCommand | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(360000) | ||
30 | |||
31 | const servers = await Promise.all([ | ||
32 | createSingleServer(1), | ||
33 | createSingleServer(2) | ||
34 | ]) | ||
35 | server = servers[0] | ||
36 | remoteServer = servers[1] | ||
37 | |||
38 | await setAccessTokensToServers([ server, remoteServer ]) | ||
39 | await setDefaultVideoChannel([ server, remoteServer ]) | ||
40 | await setDefaultChannelAvatar(server) | ||
41 | await setDefaultAccountAvatar(servers) | ||
42 | |||
43 | { | ||
44 | const attributes1 = { | ||
45 | name: '1111 2222 3333', | ||
46 | fixture: '60fps_720p_small.mp4', // 2 seconds | ||
47 | category: 1, | ||
48 | licence: 1, | ||
49 | nsfw: false, | ||
50 | language: 'fr' | ||
51 | } | ||
52 | await server.videos.upload({ attributes: attributes1 }) | ||
53 | |||
54 | const attributes2 = { ...attributes1, name: attributes1.name + ' - 2', fixture: 'video_short.mp4' } | ||
55 | await server.videos.upload({ attributes: attributes2 }) | ||
56 | |||
57 | { | ||
58 | const attributes3 = { ...attributes1, name: attributes1.name + ' - 3', language: undefined } | ||
59 | const { id, uuid, shortUUID } = await server.videos.upload({ attributes: attributes3 }) | ||
60 | videoUUID = uuid | ||
61 | videoShortUUID = shortUUID | ||
62 | |||
63 | await server.captions.add({ | ||
64 | language: 'en', | ||
65 | videoId: id, | ||
66 | fixture: 'subtitle-good2.vtt', | ||
67 | mimeType: 'application/octet-stream' | ||
68 | }) | ||
69 | |||
70 | await server.captions.add({ | ||
71 | language: 'aa', | ||
72 | videoId: id, | ||
73 | fixture: 'subtitle-good2.vtt', | ||
74 | mimeType: 'application/octet-stream' | ||
75 | }) | ||
76 | } | ||
77 | |||
78 | const attributes4 = { ...attributes1, name: attributes1.name + ' - 4', language: 'pl', nsfw: true } | ||
79 | await server.videos.upload({ attributes: attributes4 }) | ||
80 | |||
81 | await wait(1000) | ||
82 | |||
83 | startDate = new Date().toISOString() | ||
84 | |||
85 | const attributes5 = { ...attributes1, name: attributes1.name + ' - 5', licence: 2, language: undefined } | ||
86 | await server.videos.upload({ attributes: attributes5 }) | ||
87 | |||
88 | const attributes6 = { ...attributes1, name: attributes1.name + ' - 6', tags: [ 't1', 't2' ] } | ||
89 | await server.videos.upload({ attributes: attributes6 }) | ||
90 | |||
91 | const attributes7 = { ...attributes1, name: attributes1.name + ' - 7', originallyPublishedAt: '2019-02-12T09:58:08.286Z' } | ||
92 | await server.videos.upload({ attributes: attributes7 }) | ||
93 | |||
94 | const attributes8 = { ...attributes1, name: attributes1.name + ' - 8', licence: 4 } | ||
95 | await server.videos.upload({ attributes: attributes8 }) | ||
96 | } | ||
97 | |||
98 | { | ||
99 | const attributes = { | ||
100 | name: '3333 4444 5555', | ||
101 | fixture: 'video_short.mp4', | ||
102 | category: 2, | ||
103 | licence: 2, | ||
104 | language: 'en' | ||
105 | } | ||
106 | await server.videos.upload({ attributes }) | ||
107 | |||
108 | await server.videos.upload({ attributes: { ...attributes, name: attributes.name + ' duplicate' } }) | ||
109 | } | ||
110 | |||
111 | { | ||
112 | const attributes = { | ||
113 | name: '6666 7777 8888', | ||
114 | fixture: 'video_short.mp4', | ||
115 | category: 3, | ||
116 | licence: 3, | ||
117 | language: 'pl' | ||
118 | } | ||
119 | await server.videos.upload({ attributes }) | ||
120 | } | ||
121 | |||
122 | { | ||
123 | const attributes1 = { | ||
124 | name: '9999', | ||
125 | tags: [ 'aaaa', 'bbbb', 'cccc' ], | ||
126 | category: 1 | ||
127 | } | ||
128 | await server.videos.upload({ attributes: attributes1 }) | ||
129 | await server.videos.upload({ attributes: { ...attributes1, category: 2 } }) | ||
130 | |||
131 | await server.videos.upload({ attributes: { ...attributes1, tags: [ 'cccc', 'dddd' ] } }) | ||
132 | await server.videos.upload({ attributes: { ...attributes1, tags: [ 'eeee', 'ffff' ] } }) | ||
133 | } | ||
134 | |||
135 | { | ||
136 | const attributes1 = { | ||
137 | name: 'aaaa 2', | ||
138 | category: 1 | ||
139 | } | ||
140 | await server.videos.upload({ attributes: attributes1 }) | ||
141 | await server.videos.upload({ attributes: { ...attributes1, category: 2 } }) | ||
142 | } | ||
143 | |||
144 | { | ||
145 | await remoteServer.videos.upload({ attributes: { name: 'remote video 1' } }) | ||
146 | await remoteServer.videos.upload({ attributes: { name: 'remote video 2' } }) | ||
147 | } | ||
148 | |||
149 | await doubleFollow(server, remoteServer) | ||
150 | |||
151 | command = server.search | ||
152 | }) | ||
153 | |||
154 | it('Should make a simple search and not have results', async function () { | ||
155 | const body = await command.searchVideos({ search: 'abc' }) | ||
156 | |||
157 | expect(body.total).to.equal(0) | ||
158 | expect(body.data).to.have.lengthOf(0) | ||
159 | }) | ||
160 | |||
161 | it('Should make a simple search and have results', async function () { | ||
162 | const body = await command.searchVideos({ search: '4444 5555 duplicate' }) | ||
163 | |||
164 | expect(body.total).to.equal(2) | ||
165 | |||
166 | const videos = body.data | ||
167 | expect(videos).to.have.lengthOf(2) | ||
168 | |||
169 | // bestmatch | ||
170 | expect(videos[0].name).to.equal('3333 4444 5555 duplicate') | ||
171 | expect(videos[1].name).to.equal('3333 4444 5555') | ||
172 | }) | ||
173 | |||
174 | it('Should make a search on tags too, and have results', async function () { | ||
175 | const search = { | ||
176 | search: 'aaaa', | ||
177 | categoryOneOf: [ 1 ] | ||
178 | } | ||
179 | const body = await command.advancedVideoSearch({ search }) | ||
180 | |||
181 | expect(body.total).to.equal(2) | ||
182 | |||
183 | const videos = body.data | ||
184 | expect(videos).to.have.lengthOf(2) | ||
185 | |||
186 | // bestmatch | ||
187 | expect(videos[0].name).to.equal('aaaa 2') | ||
188 | expect(videos[1].name).to.equal('9999') | ||
189 | }) | ||
190 | |||
191 | it('Should filter on tags without a search', async function () { | ||
192 | const search = { | ||
193 | tagsAllOf: [ 'bbbb' ] | ||
194 | } | ||
195 | const body = await command.advancedVideoSearch({ search }) | ||
196 | |||
197 | expect(body.total).to.equal(2) | ||
198 | |||
199 | const videos = body.data | ||
200 | expect(videos).to.have.lengthOf(2) | ||
201 | |||
202 | expect(videos[0].name).to.equal('9999') | ||
203 | expect(videos[1].name).to.equal('9999') | ||
204 | }) | ||
205 | |||
206 | it('Should filter on category without a search', async function () { | ||
207 | const search = { | ||
208 | categoryOneOf: [ 3 ] | ||
209 | } | ||
210 | const body = await command.advancedVideoSearch({ search }) | ||
211 | |||
212 | expect(body.total).to.equal(1) | ||
213 | |||
214 | const videos = body.data | ||
215 | expect(videos).to.have.lengthOf(1) | ||
216 | |||
217 | expect(videos[0].name).to.equal('6666 7777 8888') | ||
218 | }) | ||
219 | |||
220 | it('Should search by tags (one of)', async function () { | ||
221 | const query = { | ||
222 | search: '9999', | ||
223 | categoryOneOf: [ 1 ], | ||
224 | tagsOneOf: [ 'aAaa', 'ffff' ] | ||
225 | } | ||
226 | |||
227 | { | ||
228 | const body = await command.advancedVideoSearch({ search: query }) | ||
229 | expect(body.total).to.equal(2) | ||
230 | } | ||
231 | |||
232 | { | ||
233 | const body = await command.advancedVideoSearch({ search: { ...query, tagsOneOf: [ 'blabla' ] } }) | ||
234 | expect(body.total).to.equal(0) | ||
235 | } | ||
236 | }) | ||
237 | |||
238 | it('Should search by tags (all of)', async function () { | ||
239 | const query = { | ||
240 | search: '9999', | ||
241 | categoryOneOf: [ 1 ], | ||
242 | tagsAllOf: [ 'CCcc' ] | ||
243 | } | ||
244 | |||
245 | { | ||
246 | const body = await command.advancedVideoSearch({ search: query }) | ||
247 | expect(body.total).to.equal(2) | ||
248 | } | ||
249 | |||
250 | { | ||
251 | const body = await command.advancedVideoSearch({ search: { ...query, tagsAllOf: [ 'blAbla' ] } }) | ||
252 | expect(body.total).to.equal(0) | ||
253 | } | ||
254 | |||
255 | { | ||
256 | const body = await command.advancedVideoSearch({ search: { ...query, tagsAllOf: [ 'bbbb', 'CCCC' ] } }) | ||
257 | expect(body.total).to.equal(1) | ||
258 | } | ||
259 | }) | ||
260 | |||
261 | it('Should search by category', async function () { | ||
262 | const query = { | ||
263 | search: '6666', | ||
264 | categoryOneOf: [ 3 ] | ||
265 | } | ||
266 | |||
267 | { | ||
268 | const body = await command.advancedVideoSearch({ search: query }) | ||
269 | expect(body.total).to.equal(1) | ||
270 | expect(body.data[0].name).to.equal('6666 7777 8888') | ||
271 | } | ||
272 | |||
273 | { | ||
274 | const body = await command.advancedVideoSearch({ search: { ...query, categoryOneOf: [ 2 ] } }) | ||
275 | expect(body.total).to.equal(0) | ||
276 | } | ||
277 | }) | ||
278 | |||
279 | it('Should search by licence', async function () { | ||
280 | const query = { | ||
281 | search: '4444 5555', | ||
282 | licenceOneOf: [ 2 ] | ||
283 | } | ||
284 | |||
285 | { | ||
286 | const body = await command.advancedVideoSearch({ search: query }) | ||
287 | expect(body.total).to.equal(2) | ||
288 | expect(body.data[0].name).to.equal('3333 4444 5555') | ||
289 | expect(body.data[1].name).to.equal('3333 4444 5555 duplicate') | ||
290 | } | ||
291 | |||
292 | { | ||
293 | const body = await command.advancedVideoSearch({ search: { ...query, licenceOneOf: [ 3 ] } }) | ||
294 | expect(body.total).to.equal(0) | ||
295 | } | ||
296 | }) | ||
297 | |||
298 | it('Should search by languages', async function () { | ||
299 | const query = { | ||
300 | search: '1111 2222 3333', | ||
301 | languageOneOf: [ 'pl', 'en' ] | ||
302 | } | ||
303 | |||
304 | { | ||
305 | const body = await command.advancedVideoSearch({ search: query }) | ||
306 | expect(body.total).to.equal(2) | ||
307 | expect(body.data[0].name).to.equal('1111 2222 3333 - 3') | ||
308 | expect(body.data[1].name).to.equal('1111 2222 3333 - 4') | ||
309 | } | ||
310 | |||
311 | { | ||
312 | const body = await command.advancedVideoSearch({ search: { ...query, languageOneOf: [ 'pl', 'en', '_unknown' ] } }) | ||
313 | expect(body.total).to.equal(3) | ||
314 | expect(body.data[0].name).to.equal('1111 2222 3333 - 3') | ||
315 | expect(body.data[1].name).to.equal('1111 2222 3333 - 4') | ||
316 | expect(body.data[2].name).to.equal('1111 2222 3333 - 5') | ||
317 | } | ||
318 | |||
319 | { | ||
320 | const body = await command.advancedVideoSearch({ search: { ...query, languageOneOf: [ 'eo' ] } }) | ||
321 | expect(body.total).to.equal(0) | ||
322 | } | ||
323 | }) | ||
324 | |||
325 | it('Should search by start date', async function () { | ||
326 | const query = { | ||
327 | search: '1111 2222 3333', | ||
328 | startDate | ||
329 | } | ||
330 | |||
331 | const body = await command.advancedVideoSearch({ search: query }) | ||
332 | expect(body.total).to.equal(4) | ||
333 | |||
334 | const videos = body.data | ||
335 | expect(videos[0].name).to.equal('1111 2222 3333 - 5') | ||
336 | expect(videos[1].name).to.equal('1111 2222 3333 - 6') | ||
337 | expect(videos[2].name).to.equal('1111 2222 3333 - 7') | ||
338 | expect(videos[3].name).to.equal('1111 2222 3333 - 8') | ||
339 | }) | ||
340 | |||
341 | it('Should make an advanced search', async function () { | ||
342 | const query = { | ||
343 | search: '1111 2222 3333', | ||
344 | languageOneOf: [ 'pl', 'fr' ], | ||
345 | durationMax: 4, | ||
346 | nsfw: 'false' as 'false', | ||
347 | licenceOneOf: [ 1, 4 ] | ||
348 | } | ||
349 | |||
350 | const body = await command.advancedVideoSearch({ search: query }) | ||
351 | expect(body.total).to.equal(4) | ||
352 | |||
353 | const videos = body.data | ||
354 | expect(videos[0].name).to.equal('1111 2222 3333') | ||
355 | expect(videos[1].name).to.equal('1111 2222 3333 - 6') | ||
356 | expect(videos[2].name).to.equal('1111 2222 3333 - 7') | ||
357 | expect(videos[3].name).to.equal('1111 2222 3333 - 8') | ||
358 | }) | ||
359 | |||
360 | it('Should make an advanced search and sort results', async function () { | ||
361 | const query = { | ||
362 | search: '1111 2222 3333', | ||
363 | languageOneOf: [ 'pl', 'fr' ], | ||
364 | durationMax: 4, | ||
365 | nsfw: 'false' as 'false', | ||
366 | licenceOneOf: [ 1, 4 ], | ||
367 | sort: '-name' | ||
368 | } | ||
369 | |||
370 | const body = await command.advancedVideoSearch({ search: query }) | ||
371 | expect(body.total).to.equal(4) | ||
372 | |||
373 | const videos = body.data | ||
374 | expect(videos[0].name).to.equal('1111 2222 3333 - 8') | ||
375 | expect(videos[1].name).to.equal('1111 2222 3333 - 7') | ||
376 | expect(videos[2].name).to.equal('1111 2222 3333 - 6') | ||
377 | expect(videos[3].name).to.equal('1111 2222 3333') | ||
378 | }) | ||
379 | |||
380 | it('Should make an advanced search and only show the first result', async function () { | ||
381 | const query = { | ||
382 | search: '1111 2222 3333', | ||
383 | languageOneOf: [ 'pl', 'fr' ], | ||
384 | durationMax: 4, | ||
385 | nsfw: 'false' as 'false', | ||
386 | licenceOneOf: [ 1, 4 ], | ||
387 | sort: '-name', | ||
388 | start: 0, | ||
389 | count: 1 | ||
390 | } | ||
391 | |||
392 | const body = await command.advancedVideoSearch({ search: query }) | ||
393 | expect(body.total).to.equal(4) | ||
394 | |||
395 | const videos = body.data | ||
396 | expect(videos[0].name).to.equal('1111 2222 3333 - 8') | ||
397 | }) | ||
398 | |||
399 | it('Should make an advanced search and only show the last result', async function () { | ||
400 | const query = { | ||
401 | search: '1111 2222 3333', | ||
402 | languageOneOf: [ 'pl', 'fr' ], | ||
403 | durationMax: 4, | ||
404 | nsfw: 'false' as 'false', | ||
405 | licenceOneOf: [ 1, 4 ], | ||
406 | sort: '-name', | ||
407 | start: 3, | ||
408 | count: 1 | ||
409 | } | ||
410 | |||
411 | const body = await command.advancedVideoSearch({ search: query }) | ||
412 | expect(body.total).to.equal(4) | ||
413 | |||
414 | const videos = body.data | ||
415 | expect(videos[0].name).to.equal('1111 2222 3333') | ||
416 | }) | ||
417 | |||
418 | it('Should search on originally published date', async function () { | ||
419 | const baseQuery = { | ||
420 | search: '1111 2222 3333', | ||
421 | languageOneOf: [ 'pl', 'fr' ], | ||
422 | durationMax: 4, | ||
423 | nsfw: 'false' as 'false', | ||
424 | licenceOneOf: [ 1, 4 ] | ||
425 | } | ||
426 | |||
427 | { | ||
428 | const query = { ...baseQuery, originallyPublishedStartDate: '2019-02-11T09:58:08.286Z' } | ||
429 | const body = await command.advancedVideoSearch({ search: query }) | ||
430 | |||
431 | expect(body.total).to.equal(1) | ||
432 | expect(body.data[0].name).to.equal('1111 2222 3333 - 7') | ||
433 | } | ||
434 | |||
435 | { | ||
436 | const query = { ...baseQuery, originallyPublishedEndDate: '2019-03-11T09:58:08.286Z' } | ||
437 | const body = await command.advancedVideoSearch({ search: query }) | ||
438 | |||
439 | expect(body.total).to.equal(1) | ||
440 | expect(body.data[0].name).to.equal('1111 2222 3333 - 7') | ||
441 | } | ||
442 | |||
443 | { | ||
444 | const query = { ...baseQuery, originallyPublishedEndDate: '2019-01-11T09:58:08.286Z' } | ||
445 | const body = await command.advancedVideoSearch({ search: query }) | ||
446 | |||
447 | expect(body.total).to.equal(0) | ||
448 | } | ||
449 | |||
450 | { | ||
451 | const query = { ...baseQuery, originallyPublishedStartDate: '2019-03-11T09:58:08.286Z' } | ||
452 | const body = await command.advancedVideoSearch({ search: query }) | ||
453 | |||
454 | expect(body.total).to.equal(0) | ||
455 | } | ||
456 | |||
457 | { | ||
458 | const query = { | ||
459 | ...baseQuery, | ||
460 | originallyPublishedStartDate: '2019-01-11T09:58:08.286Z', | ||
461 | originallyPublishedEndDate: '2019-01-10T09:58:08.286Z' | ||
462 | } | ||
463 | const body = await command.advancedVideoSearch({ search: query }) | ||
464 | |||
465 | expect(body.total).to.equal(0) | ||
466 | } | ||
467 | |||
468 | { | ||
469 | const query = { | ||
470 | ...baseQuery, | ||
471 | originallyPublishedStartDate: '2019-01-11T09:58:08.286Z', | ||
472 | originallyPublishedEndDate: '2019-04-11T09:58:08.286Z' | ||
473 | } | ||
474 | const body = await command.advancedVideoSearch({ search: query }) | ||
475 | |||
476 | expect(body.total).to.equal(1) | ||
477 | expect(body.data[0].name).to.equal('1111 2222 3333 - 7') | ||
478 | } | ||
479 | }) | ||
480 | |||
481 | it('Should search by UUID', async function () { | ||
482 | const search = videoUUID | ||
483 | const body = await command.advancedVideoSearch({ search: { search } }) | ||
484 | |||
485 | expect(body.total).to.equal(1) | ||
486 | expect(body.data[0].name).to.equal('1111 2222 3333 - 3') | ||
487 | }) | ||
488 | |||
489 | it('Should filter by UUIDs', async function () { | ||
490 | for (const uuid of [ videoUUID, videoShortUUID ]) { | ||
491 | const body = await command.advancedVideoSearch({ search: { uuids: [ uuid ] } }) | ||
492 | |||
493 | expect(body.total).to.equal(1) | ||
494 | expect(body.data[0].name).to.equal('1111 2222 3333 - 3') | ||
495 | } | ||
496 | |||
497 | { | ||
498 | const body = await command.advancedVideoSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } }) | ||
499 | |||
500 | expect(body.total).to.equal(0) | ||
501 | expect(body.data).to.have.lengthOf(0) | ||
502 | } | ||
503 | }) | ||
504 | |||
505 | it('Should search by host', async function () { | ||
506 | { | ||
507 | const body = await command.advancedVideoSearch({ search: { search: '6666 7777 8888', host: server.host } }) | ||
508 | expect(body.total).to.equal(1) | ||
509 | expect(body.data[0].name).to.equal('6666 7777 8888') | ||
510 | } | ||
511 | |||
512 | { | ||
513 | const body = await command.advancedVideoSearch({ search: { search: '1111', host: 'example.com' } }) | ||
514 | expect(body.total).to.equal(0) | ||
515 | expect(body.data).to.have.lengthOf(0) | ||
516 | } | ||
517 | |||
518 | { | ||
519 | const body = await command.advancedVideoSearch({ search: { search: 'remote', host: remoteServer.host } }) | ||
520 | expect(body.total).to.equal(2) | ||
521 | expect(body.data).to.have.lengthOf(2) | ||
522 | expect(body.data[0].name).to.equal('remote video 1') | ||
523 | expect(body.data[1].name).to.equal('remote video 2') | ||
524 | } | ||
525 | }) | ||
526 | |||
527 | it('Should search by live', async function () { | ||
528 | this.timeout(120000) | ||
529 | |||
530 | { | ||
531 | const newConfig = { | ||
532 | search: { | ||
533 | searchIndex: { enabled: false } | ||
534 | }, | ||
535 | live: { enabled: true } | ||
536 | } | ||
537 | await server.config.updateCustomSubConfig({ newConfig }) | ||
538 | } | ||
539 | |||
540 | { | ||
541 | const body = await command.advancedVideoSearch({ search: { isLive: true } }) | ||
542 | |||
543 | expect(body.total).to.equal(0) | ||
544 | expect(body.data).to.have.lengthOf(0) | ||
545 | } | ||
546 | |||
547 | { | ||
548 | const liveCommand = server.live | ||
549 | |||
550 | const liveAttributes = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.store.channel.id } | ||
551 | const live = await liveCommand.create({ fields: liveAttributes }) | ||
552 | |||
553 | const ffmpegCommand = await liveCommand.sendRTMPStreamInVideo({ videoId: live.id }) | ||
554 | await liveCommand.waitUntilPublished({ videoId: live.id }) | ||
555 | |||
556 | const body = await command.advancedVideoSearch({ search: { isLive: true } }) | ||
557 | |||
558 | expect(body.total).to.equal(1) | ||
559 | expect(body.data[0].name).to.equal('live') | ||
560 | |||
561 | await stopFfmpeg(ffmpegCommand) | ||
562 | } | ||
563 | }) | ||
564 | |||
565 | after(async function () { | ||
566 | await cleanupTests([ server ]) | ||
567 | }) | ||
568 | }) | ||
diff --git a/packages/tests/src/api/server/auto-follows.ts b/packages/tests/src/api/server/auto-follows.ts new file mode 100644 index 000000000..aa272ebcc --- /dev/null +++ b/packages/tests/src/api/server/auto-follows.ts | |||
@@ -0,0 +1,189 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockInstancesIndex } from '@tests/shared/mock-servers/index.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands' | ||
7 | |||
8 | async function checkFollow (follower: PeerTubeServer, following: PeerTubeServer, exists: boolean) { | ||
9 | { | ||
10 | const body = await following.follows.getFollowers({ start: 0, count: 5, sort: '-createdAt' }) | ||
11 | const follow = body.data.find(f => f.follower.host === follower.host && f.state === 'accepted') | ||
12 | |||
13 | if (exists === true) expect(follow, `Follower ${follower.url} should exist on ${following.url}`).to.exist | ||
14 | else expect(follow, `Follower ${follower.url} should not exist on ${following.url}`).to.be.undefined | ||
15 | } | ||
16 | |||
17 | { | ||
18 | const body = await follower.follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' }) | ||
19 | const follow = body.data.find(f => f.following.host === following.host && f.state === 'accepted') | ||
20 | |||
21 | if (exists === true) expect(follow, `Following ${following.url} should exist on ${follower.url}`).to.exist | ||
22 | else expect(follow, `Following ${following.url} should not exist on ${follower.url}`).to.be.undefined | ||
23 | } | ||
24 | } | ||
25 | |||
26 | async function server1Follows2 (servers: PeerTubeServer[]) { | ||
27 | await servers[0].follows.follow({ hosts: [ servers[1].host ] }) | ||
28 | |||
29 | await waitJobs(servers) | ||
30 | } | ||
31 | |||
32 | async function resetFollows (servers: PeerTubeServer[]) { | ||
33 | try { | ||
34 | await servers[0].follows.unfollow({ target: servers[1] }) | ||
35 | await servers[1].follows.unfollow({ target: servers[0] }) | ||
36 | } catch { /* empty */ | ||
37 | } | ||
38 | |||
39 | await waitJobs(servers) | ||
40 | |||
41 | await checkFollow(servers[0], servers[1], false) | ||
42 | await checkFollow(servers[1], servers[0], false) | ||
43 | } | ||
44 | |||
45 | describe('Test auto follows', function () { | ||
46 | let servers: PeerTubeServer[] = [] | ||
47 | |||
48 | before(async function () { | ||
49 | this.timeout(120000) | ||
50 | |||
51 | servers = await createMultipleServers(3) | ||
52 | |||
53 | // Get the access tokens | ||
54 | await setAccessTokensToServers(servers) | ||
55 | }) | ||
56 | |||
57 | describe('Auto follow back', function () { | ||
58 | |||
59 | it('Should not auto follow back if the option is not enabled', async function () { | ||
60 | this.timeout(15000) | ||
61 | |||
62 | await server1Follows2(servers) | ||
63 | |||
64 | await checkFollow(servers[0], servers[1], true) | ||
65 | await checkFollow(servers[1], servers[0], false) | ||
66 | |||
67 | await resetFollows(servers) | ||
68 | }) | ||
69 | |||
70 | it('Should auto follow back on auto accept if the option is enabled', async function () { | ||
71 | this.timeout(15000) | ||
72 | |||
73 | const config = { | ||
74 | followings: { | ||
75 | instance: { | ||
76 | autoFollowBack: { enabled: true } | ||
77 | } | ||
78 | } | ||
79 | } | ||
80 | await servers[1].config.updateCustomSubConfig({ newConfig: config }) | ||
81 | |||
82 | await server1Follows2(servers) | ||
83 | |||
84 | await checkFollow(servers[0], servers[1], true) | ||
85 | await checkFollow(servers[1], servers[0], true) | ||
86 | |||
87 | await resetFollows(servers) | ||
88 | }) | ||
89 | |||
90 | it('Should wait the acceptation before auto follow back', async function () { | ||
91 | this.timeout(30000) | ||
92 | |||
93 | const config = { | ||
94 | followings: { | ||
95 | instance: { | ||
96 | autoFollowBack: { enabled: true } | ||
97 | } | ||
98 | }, | ||
99 | followers: { | ||
100 | instance: { | ||
101 | manualApproval: true | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | await servers[1].config.updateCustomSubConfig({ newConfig: config }) | ||
106 | |||
107 | await server1Follows2(servers) | ||
108 | |||
109 | await checkFollow(servers[0], servers[1], false) | ||
110 | await checkFollow(servers[1], servers[0], false) | ||
111 | |||
112 | await servers[1].follows.acceptFollower({ follower: 'peertube@' + servers[0].host }) | ||
113 | await waitJobs(servers) | ||
114 | |||
115 | await checkFollow(servers[0], servers[1], true) | ||
116 | await checkFollow(servers[1], servers[0], true) | ||
117 | |||
118 | await resetFollows(servers) | ||
119 | |||
120 | config.followings.instance.autoFollowBack.enabled = false | ||
121 | config.followers.instance.manualApproval = false | ||
122 | await servers[1].config.updateCustomSubConfig({ newConfig: config }) | ||
123 | }) | ||
124 | }) | ||
125 | |||
126 | describe('Auto follow index', function () { | ||
127 | const instanceIndexServer = new MockInstancesIndex() | ||
128 | let port: number | ||
129 | |||
130 | before(async function () { | ||
131 | port = await instanceIndexServer.initialize() | ||
132 | }) | ||
133 | |||
134 | it('Should not auto follow index if the option is not enabled', async function () { | ||
135 | this.timeout(30000) | ||
136 | |||
137 | await wait(5000) | ||
138 | await waitJobs(servers) | ||
139 | |||
140 | await checkFollow(servers[0], servers[1], false) | ||
141 | await checkFollow(servers[1], servers[0], false) | ||
142 | }) | ||
143 | |||
144 | it('Should auto follow the index', async function () { | ||
145 | this.timeout(30000) | ||
146 | |||
147 | instanceIndexServer.addInstance(servers[1].host) | ||
148 | |||
149 | const config = { | ||
150 | followings: { | ||
151 | instance: { | ||
152 | autoFollowIndex: { | ||
153 | indexUrl: `http://127.0.0.1:${port}/api/v1/instances/hosts`, | ||
154 | enabled: true | ||
155 | } | ||
156 | } | ||
157 | } | ||
158 | } | ||
159 | await servers[0].config.updateCustomSubConfig({ newConfig: config }) | ||
160 | |||
161 | await wait(5000) | ||
162 | await waitJobs(servers) | ||
163 | |||
164 | await checkFollow(servers[0], servers[1], true) | ||
165 | |||
166 | await resetFollows(servers) | ||
167 | }) | ||
168 | |||
169 | it('Should follow new added instances in the index but not old ones', async function () { | ||
170 | this.timeout(30000) | ||
171 | |||
172 | instanceIndexServer.addInstance(servers[2].host) | ||
173 | |||
174 | await wait(5000) | ||
175 | await waitJobs(servers) | ||
176 | |||
177 | await checkFollow(servers[0], servers[1], false) | ||
178 | await checkFollow(servers[0], servers[2], true) | ||
179 | }) | ||
180 | |||
181 | after(async function () { | ||
182 | await instanceIndexServer.terminate() | ||
183 | }) | ||
184 | }) | ||
185 | |||
186 | after(async function () { | ||
187 | await cleanupTests(servers) | ||
188 | }) | ||
189 | }) | ||
diff --git a/packages/tests/src/api/server/bulk.ts b/packages/tests/src/api/server/bulk.ts new file mode 100644 index 000000000..725bcfef2 --- /dev/null +++ b/packages/tests/src/api/server/bulk.ts | |||
@@ -0,0 +1,185 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | BulkCommand, | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test bulk actions', function () { | ||
15 | const commentsUser3: { videoId: number, commentId: number }[] = [] | ||
16 | |||
17 | let servers: PeerTubeServer[] = [] | ||
18 | let user1Token: string | ||
19 | let user2Token: string | ||
20 | let user3Token: string | ||
21 | |||
22 | let bulkCommand: BulkCommand | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(120000) | ||
26 | |||
27 | servers = await createMultipleServers(2) | ||
28 | |||
29 | // Get the access tokens | ||
30 | await setAccessTokensToServers(servers) | ||
31 | |||
32 | { | ||
33 | const user = { username: 'user1', password: 'password' } | ||
34 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
35 | |||
36 | user1Token = await servers[0].login.getAccessToken(user) | ||
37 | } | ||
38 | |||
39 | { | ||
40 | const user = { username: 'user2', password: 'password' } | ||
41 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
42 | |||
43 | user2Token = await servers[0].login.getAccessToken(user) | ||
44 | } | ||
45 | |||
46 | { | ||
47 | const user = { username: 'user3', password: 'password' } | ||
48 | await servers[1].users.create({ username: user.username, password: user.password }) | ||
49 | |||
50 | user3Token = await servers[1].login.getAccessToken(user) | ||
51 | } | ||
52 | |||
53 | await doubleFollow(servers[0], servers[1]) | ||
54 | |||
55 | bulkCommand = new BulkCommand(servers[0]) | ||
56 | }) | ||
57 | |||
58 | describe('Bulk remove comments', function () { | ||
59 | async function checkInstanceCommentsRemoved () { | ||
60 | { | ||
61 | const { data } = await servers[0].videos.list() | ||
62 | |||
63 | // Server 1 should not have these comments anymore | ||
64 | for (const video of data) { | ||
65 | const { data } = await servers[0].comments.listThreads({ videoId: video.id }) | ||
66 | const comment = data.find(c => c.text === 'comment by user 3') | ||
67 | |||
68 | expect(comment).to.not.exist | ||
69 | } | ||
70 | } | ||
71 | |||
72 | { | ||
73 | const { data } = await servers[1].videos.list() | ||
74 | |||
75 | // Server 1 should not have these comments on videos of server 1 | ||
76 | for (const video of data) { | ||
77 | const { data } = await servers[1].comments.listThreads({ videoId: video.id }) | ||
78 | const comment = data.find(c => c.text === 'comment by user 3') | ||
79 | |||
80 | if (video.account.host === servers[0].host) { | ||
81 | expect(comment).to.not.exist | ||
82 | } else { | ||
83 | expect(comment).to.exist | ||
84 | } | ||
85 | } | ||
86 | } | ||
87 | } | ||
88 | |||
89 | before(async function () { | ||
90 | this.timeout(240000) | ||
91 | |||
92 | await servers[0].videos.upload({ attributes: { name: 'video 1 server 1' } }) | ||
93 | await servers[0].videos.upload({ attributes: { name: 'video 2 server 1' } }) | ||
94 | await servers[0].videos.upload({ token: user1Token, attributes: { name: 'video 3 server 1' } }) | ||
95 | |||
96 | await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } }) | ||
97 | |||
98 | await waitJobs(servers) | ||
99 | |||
100 | { | ||
101 | const { data } = await servers[0].videos.list() | ||
102 | for (const video of data) { | ||
103 | await servers[0].comments.createThread({ videoId: video.id, text: 'comment by root server 1' }) | ||
104 | await servers[0].comments.createThread({ token: user1Token, videoId: video.id, text: 'comment by user 1' }) | ||
105 | await servers[0].comments.createThread({ token: user2Token, videoId: video.id, text: 'comment by user 2' }) | ||
106 | } | ||
107 | } | ||
108 | |||
109 | { | ||
110 | const { data } = await servers[1].videos.list() | ||
111 | |||
112 | for (const video of data) { | ||
113 | await servers[1].comments.createThread({ videoId: video.id, text: 'comment by root server 2' }) | ||
114 | |||
115 | const comment = await servers[1].comments.createThread({ token: user3Token, videoId: video.id, text: 'comment by user 3' }) | ||
116 | commentsUser3.push({ videoId: video.id, commentId: comment.id }) | ||
117 | } | ||
118 | } | ||
119 | |||
120 | await waitJobs(servers) | ||
121 | }) | ||
122 | |||
123 | it('Should delete comments of an account on my videos', async function () { | ||
124 | this.timeout(60000) | ||
125 | |||
126 | await bulkCommand.removeCommentsOf({ | ||
127 | token: user1Token, | ||
128 | attributes: { | ||
129 | accountName: 'user2', | ||
130 | scope: 'my-videos' | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | await waitJobs(servers) | ||
135 | |||
136 | for (const server of servers) { | ||
137 | const { data } = await server.videos.list() | ||
138 | |||
139 | for (const video of data) { | ||
140 | const { data } = await server.comments.listThreads({ videoId: video.id }) | ||
141 | const comment = data.find(c => c.text === 'comment by user 2') | ||
142 | |||
143 | if (video.name === 'video 3 server 1') expect(comment).to.not.exist | ||
144 | else expect(comment).to.exist | ||
145 | } | ||
146 | } | ||
147 | }) | ||
148 | |||
149 | it('Should delete comments of an account on the instance', async function () { | ||
150 | this.timeout(60000) | ||
151 | |||
152 | await bulkCommand.removeCommentsOf({ | ||
153 | attributes: { | ||
154 | accountName: 'user3@' + servers[1].host, | ||
155 | scope: 'instance' | ||
156 | } | ||
157 | }) | ||
158 | |||
159 | await waitJobs(servers) | ||
160 | |||
161 | await checkInstanceCommentsRemoved() | ||
162 | }) | ||
163 | |||
164 | it('Should not re create the comment on video update', async function () { | ||
165 | this.timeout(60000) | ||
166 | |||
167 | for (const obj of commentsUser3) { | ||
168 | await servers[1].comments.addReply({ | ||
169 | token: user3Token, | ||
170 | videoId: obj.videoId, | ||
171 | toCommentId: obj.commentId, | ||
172 | text: 'comment by user 3 bis' | ||
173 | }) | ||
174 | } | ||
175 | |||
176 | await waitJobs(servers) | ||
177 | |||
178 | await checkInstanceCommentsRemoved() | ||
179 | }) | ||
180 | }) | ||
181 | |||
182 | after(async function () { | ||
183 | await cleanupTests(servers) | ||
184 | }) | ||
185 | }) | ||
diff --git a/packages/tests/src/api/server/config-defaults.ts b/packages/tests/src/api/server/config-defaults.ts new file mode 100644 index 000000000..e874af012 --- /dev/null +++ b/packages/tests/src/api/server/config-defaults.ts | |||
@@ -0,0 +1,294 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoDetails, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
13 | |||
14 | describe('Test config defaults', function () { | ||
15 | let server: PeerTubeServer | ||
16 | let channelId: number | ||
17 | |||
18 | before(async function () { | ||
19 | this.timeout(30000) | ||
20 | |||
21 | server = await createSingleServer(1) | ||
22 | await setAccessTokensToServers([ server ]) | ||
23 | await setDefaultVideoChannel([ server ]) | ||
24 | |||
25 | channelId = server.store.channel.id | ||
26 | }) | ||
27 | |||
28 | describe('Default publish values', function () { | ||
29 | |||
30 | before(async function () { | ||
31 | const overrideConfig = { | ||
32 | defaults: { | ||
33 | publish: { | ||
34 | comments_enabled: false, | ||
35 | download_enabled: false, | ||
36 | privacy: VideoPrivacy.INTERNAL, | ||
37 | licence: 4 | ||
38 | } | ||
39 | } | ||
40 | } | ||
41 | |||
42 | await server.kill() | ||
43 | await server.run(overrideConfig) | ||
44 | }) | ||
45 | |||
46 | const attributes = { | ||
47 | name: 'video', | ||
48 | downloadEnabled: undefined, | ||
49 | commentsEnabled: undefined, | ||
50 | licence: undefined, | ||
51 | privacy: VideoPrivacy.PUBLIC // Privacy is mandatory for server | ||
52 | } | ||
53 | |||
54 | function checkVideo (video: VideoDetails) { | ||
55 | expect(video.downloadEnabled).to.be.false | ||
56 | expect(video.commentsEnabled).to.be.false | ||
57 | expect(video.licence.id).to.equal(4) | ||
58 | } | ||
59 | |||
60 | before(async function () { | ||
61 | await server.config.disableTranscoding() | ||
62 | await server.config.enableImports() | ||
63 | await server.config.enableLive({ allowReplay: false, transcoding: false }) | ||
64 | }) | ||
65 | |||
66 | it('Should have the correct server configuration', async function () { | ||
67 | const config = await server.config.getConfig() | ||
68 | |||
69 | expect(config.defaults.publish.commentsEnabled).to.be.false | ||
70 | expect(config.defaults.publish.downloadEnabled).to.be.false | ||
71 | expect(config.defaults.publish.licence).to.equal(4) | ||
72 | expect(config.defaults.publish.privacy).to.equal(VideoPrivacy.INTERNAL) | ||
73 | }) | ||
74 | |||
75 | it('Should respect default values when uploading a video', async function () { | ||
76 | for (const mode of [ 'legacy' as 'legacy', 'resumable' as 'resumable' ]) { | ||
77 | const { id } = await server.videos.upload({ attributes, mode }) | ||
78 | |||
79 | const video = await server.videos.get({ id }) | ||
80 | checkVideo(video) | ||
81 | } | ||
82 | }) | ||
83 | |||
84 | it('Should respect default values when importing a video using URL', async function () { | ||
85 | const { video: { id } } = await server.imports.importVideo({ | ||
86 | attributes: { | ||
87 | ...attributes, | ||
88 | channelId, | ||
89 | targetUrl: FIXTURE_URLS.goodVideo | ||
90 | } | ||
91 | }) | ||
92 | |||
93 | const video = await server.videos.get({ id }) | ||
94 | checkVideo(video) | ||
95 | }) | ||
96 | |||
97 | it('Should respect default values when importing a video using magnet URI', async function () { | ||
98 | const { video: { id } } = await server.imports.importVideo({ | ||
99 | attributes: { | ||
100 | ...attributes, | ||
101 | channelId, | ||
102 | magnetUri: FIXTURE_URLS.magnet | ||
103 | } | ||
104 | }) | ||
105 | |||
106 | const video = await server.videos.get({ id }) | ||
107 | checkVideo(video) | ||
108 | }) | ||
109 | |||
110 | it('Should respect default values when creating a live', async function () { | ||
111 | const { id } = await server.live.create({ | ||
112 | fields: { | ||
113 | ...attributes, | ||
114 | channelId | ||
115 | } | ||
116 | }) | ||
117 | |||
118 | const video = await server.videos.get({ id }) | ||
119 | checkVideo(video) | ||
120 | }) | ||
121 | }) | ||
122 | |||
123 | describe('Default P2P values', function () { | ||
124 | |||
125 | describe('Webapp default value', function () { | ||
126 | |||
127 | before(async function () { | ||
128 | const overrideConfig = { | ||
129 | defaults: { | ||
130 | p2p: { | ||
131 | webapp: { | ||
132 | enabled: false | ||
133 | } | ||
134 | } | ||
135 | } | ||
136 | } | ||
137 | |||
138 | await server.kill() | ||
139 | await server.run(overrideConfig) | ||
140 | }) | ||
141 | |||
142 | it('Should have appropriate P2P config', async function () { | ||
143 | const config = await server.config.getConfig() | ||
144 | |||
145 | expect(config.defaults.p2p.webapp.enabled).to.be.false | ||
146 | expect(config.defaults.p2p.embed.enabled).to.be.true | ||
147 | }) | ||
148 | |||
149 | it('Should create a user with this default setting', async function () { | ||
150 | await server.users.create({ username: 'user_p2p_1' }) | ||
151 | const userToken = await server.login.getAccessToken('user_p2p_1') | ||
152 | |||
153 | const { p2pEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
154 | expect(p2pEnabled).to.be.false | ||
155 | }) | ||
156 | |||
157 | it('Should register a user with this default setting', async function () { | ||
158 | await server.registrations.register({ username: 'user_p2p_2' }) | ||
159 | |||
160 | const userToken = await server.login.getAccessToken('user_p2p_2') | ||
161 | |||
162 | const { p2pEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
163 | expect(p2pEnabled).to.be.false | ||
164 | }) | ||
165 | }) | ||
166 | |||
167 | describe('Embed default value', function () { | ||
168 | |||
169 | before(async function () { | ||
170 | const overrideConfig = { | ||
171 | defaults: { | ||
172 | p2p: { | ||
173 | embed: { | ||
174 | enabled: false | ||
175 | } | ||
176 | } | ||
177 | }, | ||
178 | signup: { | ||
179 | limit: 15 | ||
180 | } | ||
181 | } | ||
182 | |||
183 | await server.kill() | ||
184 | await server.run(overrideConfig) | ||
185 | }) | ||
186 | |||
187 | it('Should have appropriate P2P config', async function () { | ||
188 | const config = await server.config.getConfig() | ||
189 | |||
190 | expect(config.defaults.p2p.webapp.enabled).to.be.true | ||
191 | expect(config.defaults.p2p.embed.enabled).to.be.false | ||
192 | }) | ||
193 | |||
194 | it('Should create a user with this default setting', async function () { | ||
195 | await server.users.create({ username: 'user_p2p_3' }) | ||
196 | const userToken = await server.login.getAccessToken('user_p2p_3') | ||
197 | |||
198 | const { p2pEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
199 | expect(p2pEnabled).to.be.true | ||
200 | }) | ||
201 | |||
202 | it('Should register a user with this default setting', async function () { | ||
203 | await server.registrations.register({ username: 'user_p2p_4' }) | ||
204 | |||
205 | const userToken = await server.login.getAccessToken('user_p2p_4') | ||
206 | |||
207 | const { p2pEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
208 | expect(p2pEnabled).to.be.true | ||
209 | }) | ||
210 | }) | ||
211 | }) | ||
212 | |||
213 | describe('Default user attributes', function () { | ||
214 | it('Should create a user and register a user with the default config', async function () { | ||
215 | await server.config.updateCustomSubConfig({ | ||
216 | newConfig: { | ||
217 | user: { | ||
218 | history: { | ||
219 | videos: { | ||
220 | enabled: true | ||
221 | } | ||
222 | }, | ||
223 | videoQuota : -1, | ||
224 | videoQuotaDaily: -1 | ||
225 | }, | ||
226 | signup: { | ||
227 | enabled: true, | ||
228 | requiresApproval: false | ||
229 | } | ||
230 | } | ||
231 | }) | ||
232 | |||
233 | const config = await server.config.getConfig() | ||
234 | |||
235 | expect(config.user.videoQuota).to.equal(-1) | ||
236 | expect(config.user.videoQuotaDaily).to.equal(-1) | ||
237 | |||
238 | const user1Token = await server.users.generateUserAndToken('user1') | ||
239 | const user1 = await server.users.getMyInfo({ token: user1Token }) | ||
240 | |||
241 | const user = { displayName: 'super user 2', username: 'user2', password: 'super password' } | ||
242 | const channel = { name: 'my_user_2_channel', displayName: 'my channel' } | ||
243 | await server.registrations.register({ ...user, channel }) | ||
244 | const user2Token = await server.login.getAccessToken(user) | ||
245 | const user2 = await server.users.getMyInfo({ token: user2Token }) | ||
246 | |||
247 | for (const user of [ user1, user2 ]) { | ||
248 | expect(user.videosHistoryEnabled).to.be.true | ||
249 | expect(user.videoQuota).to.equal(-1) | ||
250 | expect(user.videoQuotaDaily).to.equal(-1) | ||
251 | } | ||
252 | }) | ||
253 | |||
254 | it('Should update config and create a user and register a user with the new default config', async function () { | ||
255 | await server.config.updateCustomSubConfig({ | ||
256 | newConfig: { | ||
257 | user: { | ||
258 | history: { | ||
259 | videos: { | ||
260 | enabled: false | ||
261 | } | ||
262 | }, | ||
263 | videoQuota : 5242881, | ||
264 | videoQuotaDaily: 318742 | ||
265 | }, | ||
266 | signup: { | ||
267 | enabled: true, | ||
268 | requiresApproval: false | ||
269 | } | ||
270 | } | ||
271 | }) | ||
272 | |||
273 | const user3Token = await server.users.generateUserAndToken('user3') | ||
274 | const user3 = await server.users.getMyInfo({ token: user3Token }) | ||
275 | |||
276 | const user = { displayName: 'super user 4', username: 'user4', password: 'super password' } | ||
277 | const channel = { name: 'my_user_4_channel', displayName: 'my channel' } | ||
278 | await server.registrations.register({ ...user, channel }) | ||
279 | const user4Token = await server.login.getAccessToken(user) | ||
280 | const user4 = await server.users.getMyInfo({ token: user4Token }) | ||
281 | |||
282 | for (const user of [ user3, user4 ]) { | ||
283 | expect(user.videosHistoryEnabled).to.be.false | ||
284 | expect(user.videoQuota).to.equal(5242881) | ||
285 | expect(user.videoQuotaDaily).to.equal(318742) | ||
286 | } | ||
287 | }) | ||
288 | |||
289 | }) | ||
290 | |||
291 | after(async function () { | ||
292 | await cleanupTests([ server ]) | ||
293 | }) | ||
294 | }) | ||
diff --git a/packages/tests/src/api/server/config.ts b/packages/tests/src/api/server/config.ts new file mode 100644 index 000000000..ce64668f8 --- /dev/null +++ b/packages/tests/src/api/server/config.ts | |||
@@ -0,0 +1,645 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { parallelTests } from '@peertube/peertube-node-utils' | ||
5 | import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | killallServers, | ||
10 | makeGetRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { | ||
16 | expect(data.instance.name).to.equal('PeerTube') | ||
17 | expect(data.instance.shortDescription).to.equal( | ||
18 | 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.' | ||
19 | ) | ||
20 | expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') | ||
21 | |||
22 | expect(data.instance.terms).to.equal('No terms for now.') | ||
23 | expect(data.instance.creationReason).to.be.empty | ||
24 | expect(data.instance.codeOfConduct).to.be.empty | ||
25 | expect(data.instance.moderationInformation).to.be.empty | ||
26 | expect(data.instance.administrator).to.be.empty | ||
27 | expect(data.instance.maintenanceLifetime).to.be.empty | ||
28 | expect(data.instance.businessModel).to.be.empty | ||
29 | expect(data.instance.hardwareInformation).to.be.empty | ||
30 | |||
31 | expect(data.instance.languages).to.have.lengthOf(0) | ||
32 | expect(data.instance.categories).to.have.lengthOf(0) | ||
33 | |||
34 | expect(data.instance.defaultClientRoute).to.equal('/videos/trending') | ||
35 | expect(data.instance.isNSFW).to.be.false | ||
36 | expect(data.instance.defaultNSFWPolicy).to.equal('display') | ||
37 | expect(data.instance.customizations.css).to.be.empty | ||
38 | expect(data.instance.customizations.javascript).to.be.empty | ||
39 | |||
40 | expect(data.services.twitter.username).to.equal('@Chocobozzz') | ||
41 | expect(data.services.twitter.whitelisted).to.be.false | ||
42 | |||
43 | expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.false | ||
44 | expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false | ||
45 | |||
46 | expect(data.cache.previews.size).to.equal(1) | ||
47 | expect(data.cache.captions.size).to.equal(1) | ||
48 | expect(data.cache.torrents.size).to.equal(1) | ||
49 | expect(data.cache.storyboards.size).to.equal(1) | ||
50 | |||
51 | expect(data.signup.enabled).to.be.true | ||
52 | expect(data.signup.limit).to.equal(4) | ||
53 | expect(data.signup.minimumAge).to.equal(16) | ||
54 | expect(data.signup.requiresApproval).to.be.false | ||
55 | expect(data.signup.requiresEmailVerification).to.be.false | ||
56 | |||
57 | expect(data.admin.email).to.equal('admin' + server.internalServerNumber + '@example.com') | ||
58 | expect(data.contactForm.enabled).to.be.true | ||
59 | |||
60 | expect(data.user.history.videos.enabled).to.be.true | ||
61 | expect(data.user.videoQuota).to.equal(5242880) | ||
62 | expect(data.user.videoQuotaDaily).to.equal(-1) | ||
63 | |||
64 | expect(data.videoChannels.maxPerUser).to.equal(20) | ||
65 | |||
66 | expect(data.transcoding.enabled).to.be.false | ||
67 | expect(data.transcoding.remoteRunners.enabled).to.be.false | ||
68 | expect(data.transcoding.allowAdditionalExtensions).to.be.false | ||
69 | expect(data.transcoding.allowAudioFiles).to.be.false | ||
70 | expect(data.transcoding.threads).to.equal(2) | ||
71 | expect(data.transcoding.concurrency).to.equal(2) | ||
72 | expect(data.transcoding.profile).to.equal('default') | ||
73 | expect(data.transcoding.resolutions['144p']).to.be.false | ||
74 | expect(data.transcoding.resolutions['240p']).to.be.true | ||
75 | expect(data.transcoding.resolutions['360p']).to.be.true | ||
76 | expect(data.transcoding.resolutions['480p']).to.be.true | ||
77 | expect(data.transcoding.resolutions['720p']).to.be.true | ||
78 | expect(data.transcoding.resolutions['1080p']).to.be.true | ||
79 | expect(data.transcoding.resolutions['1440p']).to.be.true | ||
80 | expect(data.transcoding.resolutions['2160p']).to.be.true | ||
81 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true | ||
82 | expect(data.transcoding.webVideos.enabled).to.be.true | ||
83 | expect(data.transcoding.hls.enabled).to.be.true | ||
84 | |||
85 | expect(data.live.enabled).to.be.false | ||
86 | expect(data.live.allowReplay).to.be.false | ||
87 | expect(data.live.latencySetting.enabled).to.be.true | ||
88 | expect(data.live.maxDuration).to.equal(-1) | ||
89 | expect(data.live.maxInstanceLives).to.equal(20) | ||
90 | expect(data.live.maxUserLives).to.equal(3) | ||
91 | expect(data.live.transcoding.enabled).to.be.false | ||
92 | expect(data.live.transcoding.remoteRunners.enabled).to.be.false | ||
93 | expect(data.live.transcoding.threads).to.equal(2) | ||
94 | expect(data.live.transcoding.profile).to.equal('default') | ||
95 | expect(data.live.transcoding.resolutions['144p']).to.be.false | ||
96 | expect(data.live.transcoding.resolutions['240p']).to.be.false | ||
97 | expect(data.live.transcoding.resolutions['360p']).to.be.false | ||
98 | expect(data.live.transcoding.resolutions['480p']).to.be.false | ||
99 | expect(data.live.transcoding.resolutions['720p']).to.be.false | ||
100 | expect(data.live.transcoding.resolutions['1080p']).to.be.false | ||
101 | expect(data.live.transcoding.resolutions['1440p']).to.be.false | ||
102 | expect(data.live.transcoding.resolutions['2160p']).to.be.false | ||
103 | expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true | ||
104 | |||
105 | expect(data.videoStudio.enabled).to.be.false | ||
106 | expect(data.videoStudio.remoteRunners.enabled).to.be.false | ||
107 | |||
108 | expect(data.videoFile.update.enabled).to.be.false | ||
109 | |||
110 | expect(data.import.videos.concurrency).to.equal(2) | ||
111 | expect(data.import.videos.http.enabled).to.be.true | ||
112 | expect(data.import.videos.torrent.enabled).to.be.true | ||
113 | expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false | ||
114 | |||
115 | expect(data.followers.instance.enabled).to.be.true | ||
116 | expect(data.followers.instance.manualApproval).to.be.false | ||
117 | |||
118 | expect(data.followings.instance.autoFollowBack.enabled).to.be.false | ||
119 | expect(data.followings.instance.autoFollowIndex.enabled).to.be.false | ||
120 | expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('') | ||
121 | |||
122 | expect(data.broadcastMessage.enabled).to.be.false | ||
123 | expect(data.broadcastMessage.level).to.equal('info') | ||
124 | expect(data.broadcastMessage.message).to.equal('') | ||
125 | expect(data.broadcastMessage.dismissable).to.be.false | ||
126 | } | ||
127 | |||
128 | function checkUpdatedConfig (data: CustomConfig) { | ||
129 | expect(data.instance.name).to.equal('PeerTube updated') | ||
130 | expect(data.instance.shortDescription).to.equal('my short description') | ||
131 | expect(data.instance.description).to.equal('my super description') | ||
132 | |||
133 | expect(data.instance.terms).to.equal('my super terms') | ||
134 | expect(data.instance.creationReason).to.equal('my super creation reason') | ||
135 | expect(data.instance.codeOfConduct).to.equal('my super coc') | ||
136 | expect(data.instance.moderationInformation).to.equal('my super moderation information') | ||
137 | expect(data.instance.administrator).to.equal('Kuja') | ||
138 | expect(data.instance.maintenanceLifetime).to.equal('forever') | ||
139 | expect(data.instance.businessModel).to.equal('my super business model') | ||
140 | expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM') | ||
141 | |||
142 | expect(data.instance.languages).to.deep.equal([ 'en', 'es' ]) | ||
143 | expect(data.instance.categories).to.deep.equal([ 1, 2 ]) | ||
144 | |||
145 | expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') | ||
146 | expect(data.instance.isNSFW).to.be.true | ||
147 | expect(data.instance.defaultNSFWPolicy).to.equal('blur') | ||
148 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') | ||
149 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') | ||
150 | |||
151 | expect(data.services.twitter.username).to.equal('@Kuja') | ||
152 | expect(data.services.twitter.whitelisted).to.be.true | ||
153 | |||
154 | expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.true | ||
155 | expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.true | ||
156 | |||
157 | expect(data.cache.previews.size).to.equal(2) | ||
158 | expect(data.cache.captions.size).to.equal(3) | ||
159 | expect(data.cache.torrents.size).to.equal(4) | ||
160 | expect(data.cache.storyboards.size).to.equal(5) | ||
161 | |||
162 | expect(data.signup.enabled).to.be.false | ||
163 | expect(data.signup.limit).to.equal(5) | ||
164 | expect(data.signup.requiresApproval).to.be.false | ||
165 | expect(data.signup.requiresEmailVerification).to.be.false | ||
166 | expect(data.signup.minimumAge).to.equal(10) | ||
167 | |||
168 | // We override admin email in parallel tests, so skip this exception | ||
169 | if (parallelTests() === false) { | ||
170 | expect(data.admin.email).to.equal('superadmin1@example.com') | ||
171 | } | ||
172 | |||
173 | expect(data.contactForm.enabled).to.be.false | ||
174 | |||
175 | expect(data.user.history.videos.enabled).to.be.false | ||
176 | expect(data.user.videoQuota).to.equal(5242881) | ||
177 | expect(data.user.videoQuotaDaily).to.equal(318742) | ||
178 | |||
179 | expect(data.videoChannels.maxPerUser).to.equal(24) | ||
180 | |||
181 | expect(data.transcoding.enabled).to.be.true | ||
182 | expect(data.transcoding.remoteRunners.enabled).to.be.true | ||
183 | expect(data.transcoding.threads).to.equal(1) | ||
184 | expect(data.transcoding.concurrency).to.equal(3) | ||
185 | expect(data.transcoding.allowAdditionalExtensions).to.be.true | ||
186 | expect(data.transcoding.allowAudioFiles).to.be.true | ||
187 | expect(data.transcoding.profile).to.equal('vod_profile') | ||
188 | expect(data.transcoding.resolutions['144p']).to.be.false | ||
189 | expect(data.transcoding.resolutions['240p']).to.be.false | ||
190 | expect(data.transcoding.resolutions['360p']).to.be.true | ||
191 | expect(data.transcoding.resolutions['480p']).to.be.true | ||
192 | expect(data.transcoding.resolutions['720p']).to.be.false | ||
193 | expect(data.transcoding.resolutions['1080p']).to.be.false | ||
194 | expect(data.transcoding.resolutions['2160p']).to.be.false | ||
195 | expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false | ||
196 | expect(data.transcoding.hls.enabled).to.be.false | ||
197 | expect(data.transcoding.webVideos.enabled).to.be.true | ||
198 | |||
199 | expect(data.live.enabled).to.be.true | ||
200 | expect(data.live.allowReplay).to.be.true | ||
201 | expect(data.live.latencySetting.enabled).to.be.false | ||
202 | expect(data.live.maxDuration).to.equal(5000) | ||
203 | expect(data.live.maxInstanceLives).to.equal(-1) | ||
204 | expect(data.live.maxUserLives).to.equal(10) | ||
205 | expect(data.live.transcoding.enabled).to.be.true | ||
206 | expect(data.live.transcoding.remoteRunners.enabled).to.be.true | ||
207 | expect(data.live.transcoding.threads).to.equal(4) | ||
208 | expect(data.live.transcoding.profile).to.equal('live_profile') | ||
209 | expect(data.live.transcoding.resolutions['144p']).to.be.true | ||
210 | expect(data.live.transcoding.resolutions['240p']).to.be.true | ||
211 | expect(data.live.transcoding.resolutions['360p']).to.be.true | ||
212 | expect(data.live.transcoding.resolutions['480p']).to.be.true | ||
213 | expect(data.live.transcoding.resolutions['720p']).to.be.true | ||
214 | expect(data.live.transcoding.resolutions['1080p']).to.be.true | ||
215 | expect(data.live.transcoding.resolutions['2160p']).to.be.true | ||
216 | expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.false | ||
217 | |||
218 | expect(data.videoStudio.enabled).to.be.true | ||
219 | expect(data.videoStudio.remoteRunners.enabled).to.be.true | ||
220 | |||
221 | expect(data.videoFile.update.enabled).to.be.true | ||
222 | |||
223 | expect(data.import.videos.concurrency).to.equal(4) | ||
224 | expect(data.import.videos.http.enabled).to.be.false | ||
225 | expect(data.import.videos.torrent.enabled).to.be.false | ||
226 | expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true | ||
227 | |||
228 | expect(data.followers.instance.enabled).to.be.false | ||
229 | expect(data.followers.instance.manualApproval).to.be.true | ||
230 | |||
231 | expect(data.followings.instance.autoFollowBack.enabled).to.be.true | ||
232 | expect(data.followings.instance.autoFollowIndex.enabled).to.be.true | ||
233 | expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com') | ||
234 | |||
235 | expect(data.broadcastMessage.enabled).to.be.true | ||
236 | expect(data.broadcastMessage.level).to.equal('error') | ||
237 | expect(data.broadcastMessage.message).to.equal('super bad message') | ||
238 | expect(data.broadcastMessage.dismissable).to.be.true | ||
239 | } | ||
240 | |||
241 | const newCustomConfig: CustomConfig = { | ||
242 | instance: { | ||
243 | name: 'PeerTube updated', | ||
244 | shortDescription: 'my short description', | ||
245 | description: 'my super description', | ||
246 | terms: 'my super terms', | ||
247 | codeOfConduct: 'my super coc', | ||
248 | |||
249 | creationReason: 'my super creation reason', | ||
250 | moderationInformation: 'my super moderation information', | ||
251 | administrator: 'Kuja', | ||
252 | maintenanceLifetime: 'forever', | ||
253 | businessModel: 'my super business model', | ||
254 | hardwareInformation: '2vCore 3GB RAM', | ||
255 | |||
256 | languages: [ 'en', 'es' ], | ||
257 | categories: [ 1, 2 ], | ||
258 | |||
259 | isNSFW: true, | ||
260 | defaultNSFWPolicy: 'blur' as 'blur', | ||
261 | |||
262 | defaultClientRoute: '/videos/recently-added', | ||
263 | |||
264 | customizations: { | ||
265 | javascript: 'alert("coucou")', | ||
266 | css: 'body { background-color: red; }' | ||
267 | } | ||
268 | }, | ||
269 | theme: { | ||
270 | default: 'default' | ||
271 | }, | ||
272 | services: { | ||
273 | twitter: { | ||
274 | username: '@Kuja', | ||
275 | whitelisted: true | ||
276 | } | ||
277 | }, | ||
278 | client: { | ||
279 | videos: { | ||
280 | miniature: { | ||
281 | preferAuthorDisplayName: true | ||
282 | } | ||
283 | }, | ||
284 | menu: { | ||
285 | login: { | ||
286 | redirectOnSingleExternalAuth: true | ||
287 | } | ||
288 | } | ||
289 | }, | ||
290 | cache: { | ||
291 | previews: { | ||
292 | size: 2 | ||
293 | }, | ||
294 | captions: { | ||
295 | size: 3 | ||
296 | }, | ||
297 | torrents: { | ||
298 | size: 4 | ||
299 | }, | ||
300 | storyboards: { | ||
301 | size: 5 | ||
302 | } | ||
303 | }, | ||
304 | signup: { | ||
305 | enabled: false, | ||
306 | limit: 5, | ||
307 | requiresApproval: false, | ||
308 | requiresEmailVerification: false, | ||
309 | minimumAge: 10 | ||
310 | }, | ||
311 | admin: { | ||
312 | email: 'superadmin1@example.com' | ||
313 | }, | ||
314 | contactForm: { | ||
315 | enabled: false | ||
316 | }, | ||
317 | user: { | ||
318 | history: { | ||
319 | videos: { | ||
320 | enabled: false | ||
321 | } | ||
322 | }, | ||
323 | videoQuota: 5242881, | ||
324 | videoQuotaDaily: 318742 | ||
325 | }, | ||
326 | videoChannels: { | ||
327 | maxPerUser: 24 | ||
328 | }, | ||
329 | transcoding: { | ||
330 | enabled: true, | ||
331 | remoteRunners: { | ||
332 | enabled: true | ||
333 | }, | ||
334 | allowAdditionalExtensions: true, | ||
335 | allowAudioFiles: true, | ||
336 | threads: 1, | ||
337 | concurrency: 3, | ||
338 | profile: 'vod_profile', | ||
339 | resolutions: { | ||
340 | '0p': false, | ||
341 | '144p': false, | ||
342 | '240p': false, | ||
343 | '360p': true, | ||
344 | '480p': true, | ||
345 | '720p': false, | ||
346 | '1080p': false, | ||
347 | '1440p': false, | ||
348 | '2160p': false | ||
349 | }, | ||
350 | alwaysTranscodeOriginalResolution: false, | ||
351 | webVideos: { | ||
352 | enabled: true | ||
353 | }, | ||
354 | hls: { | ||
355 | enabled: false | ||
356 | } | ||
357 | }, | ||
358 | live: { | ||
359 | enabled: true, | ||
360 | allowReplay: true, | ||
361 | latencySetting: { | ||
362 | enabled: false | ||
363 | }, | ||
364 | maxDuration: 5000, | ||
365 | maxInstanceLives: -1, | ||
366 | maxUserLives: 10, | ||
367 | transcoding: { | ||
368 | enabled: true, | ||
369 | remoteRunners: { | ||
370 | enabled: true | ||
371 | }, | ||
372 | threads: 4, | ||
373 | profile: 'live_profile', | ||
374 | resolutions: { | ||
375 | '144p': true, | ||
376 | '240p': true, | ||
377 | '360p': true, | ||
378 | '480p': true, | ||
379 | '720p': true, | ||
380 | '1080p': true, | ||
381 | '1440p': true, | ||
382 | '2160p': true | ||
383 | }, | ||
384 | alwaysTranscodeOriginalResolution: false | ||
385 | } | ||
386 | }, | ||
387 | videoStudio: { | ||
388 | enabled: true, | ||
389 | remoteRunners: { | ||
390 | enabled: true | ||
391 | } | ||
392 | }, | ||
393 | videoFile: { | ||
394 | update: { | ||
395 | enabled: true | ||
396 | } | ||
397 | }, | ||
398 | import: { | ||
399 | videos: { | ||
400 | concurrency: 4, | ||
401 | http: { | ||
402 | enabled: false | ||
403 | }, | ||
404 | torrent: { | ||
405 | enabled: false | ||
406 | } | ||
407 | }, | ||
408 | videoChannelSynchronization: { | ||
409 | enabled: false, | ||
410 | maxPerUser: 10 | ||
411 | } | ||
412 | }, | ||
413 | trending: { | ||
414 | videos: { | ||
415 | algorithms: { | ||
416 | enabled: [ 'hot', 'most-viewed', 'most-liked' ], | ||
417 | default: 'hot' | ||
418 | } | ||
419 | } | ||
420 | }, | ||
421 | autoBlacklist: { | ||
422 | videos: { | ||
423 | ofUsers: { | ||
424 | enabled: true | ||
425 | } | ||
426 | } | ||
427 | }, | ||
428 | followers: { | ||
429 | instance: { | ||
430 | enabled: false, | ||
431 | manualApproval: true | ||
432 | } | ||
433 | }, | ||
434 | followings: { | ||
435 | instance: { | ||
436 | autoFollowBack: { | ||
437 | enabled: true | ||
438 | }, | ||
439 | autoFollowIndex: { | ||
440 | enabled: true, | ||
441 | indexUrl: 'https://updated.example.com' | ||
442 | } | ||
443 | } | ||
444 | }, | ||
445 | broadcastMessage: { | ||
446 | enabled: true, | ||
447 | level: 'error', | ||
448 | message: 'super bad message', | ||
449 | dismissable: true | ||
450 | }, | ||
451 | search: { | ||
452 | remoteUri: { | ||
453 | anonymous: true, | ||
454 | users: true | ||
455 | }, | ||
456 | searchIndex: { | ||
457 | enabled: true, | ||
458 | url: 'https://search.joinpeertube.org', | ||
459 | disableLocalSearch: true, | ||
460 | isDefaultSearch: true | ||
461 | } | ||
462 | } | ||
463 | } | ||
464 | |||
465 | describe('Test static config', function () { | ||
466 | let server: PeerTubeServer = null | ||
467 | |||
468 | before(async function () { | ||
469 | this.timeout(30000) | ||
470 | |||
471 | server = await createSingleServer(1, { webadmin: { configuration: { edition: { allowed: false } } } }) | ||
472 | await setAccessTokensToServers([ server ]) | ||
473 | }) | ||
474 | |||
475 | it('Should tell the client that edits are not allowed', async function () { | ||
476 | const data = await server.config.getConfig() | ||
477 | |||
478 | expect(data.webadmin.configuration.edition.allowed).to.be.false | ||
479 | }) | ||
480 | |||
481 | it('Should error when client tries to update', async function () { | ||
482 | await server.config.updateCustomConfig({ newCustomConfig, expectedStatus: 405 }) | ||
483 | }) | ||
484 | |||
485 | after(async function () { | ||
486 | await cleanupTests([ server ]) | ||
487 | }) | ||
488 | }) | ||
489 | |||
490 | describe('Test config', function () { | ||
491 | let server: PeerTubeServer = null | ||
492 | |||
493 | before(async function () { | ||
494 | this.timeout(30000) | ||
495 | |||
496 | server = await createSingleServer(1) | ||
497 | await setAccessTokensToServers([ server ]) | ||
498 | }) | ||
499 | |||
500 | it('Should have a correct config on a server with registration enabled', async function () { | ||
501 | const data = await server.config.getConfig() | ||
502 | |||
503 | expect(data.signup.allowed).to.be.true | ||
504 | }) | ||
505 | |||
506 | it('Should have a correct config on a server with registration enabled and a users limit', async function () { | ||
507 | this.timeout(5000) | ||
508 | |||
509 | await Promise.all([ | ||
510 | server.registrations.register({ username: 'user1' }), | ||
511 | server.registrations.register({ username: 'user2' }), | ||
512 | server.registrations.register({ username: 'user3' }) | ||
513 | ]) | ||
514 | |||
515 | const data = await server.config.getConfig() | ||
516 | |||
517 | expect(data.signup.allowed).to.be.false | ||
518 | }) | ||
519 | |||
520 | it('Should have the correct video allowed extensions', async function () { | ||
521 | const data = await server.config.getConfig() | ||
522 | |||
523 | expect(data.video.file.extensions).to.have.lengthOf(3) | ||
524 | expect(data.video.file.extensions).to.contain('.mp4') | ||
525 | expect(data.video.file.extensions).to.contain('.webm') | ||
526 | expect(data.video.file.extensions).to.contain('.ogv') | ||
527 | |||
528 | await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 }) | ||
529 | await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 }) | ||
530 | |||
531 | expect(data.contactForm.enabled).to.be.true | ||
532 | }) | ||
533 | |||
534 | it('Should get the customized configuration', async function () { | ||
535 | const data = await server.config.getCustomConfig() | ||
536 | |||
537 | checkInitialConfig(server, data) | ||
538 | }) | ||
539 | |||
540 | it('Should update the customized configuration', async function () { | ||
541 | await server.config.updateCustomConfig({ newCustomConfig }) | ||
542 | |||
543 | const data = await server.config.getCustomConfig() | ||
544 | checkUpdatedConfig(data) | ||
545 | }) | ||
546 | |||
547 | it('Should have the correct updated video allowed extensions', async function () { | ||
548 | this.timeout(30000) | ||
549 | |||
550 | const data = await server.config.getConfig() | ||
551 | |||
552 | expect(data.video.file.extensions).to.have.length.above(4) | ||
553 | expect(data.video.file.extensions).to.contain('.mp4') | ||
554 | expect(data.video.file.extensions).to.contain('.webm') | ||
555 | expect(data.video.file.extensions).to.contain('.ogv') | ||
556 | expect(data.video.file.extensions).to.contain('.flv') | ||
557 | expect(data.video.file.extensions).to.contain('.wmv') | ||
558 | expect(data.video.file.extensions).to.contain('.mkv') | ||
559 | expect(data.video.file.extensions).to.contain('.mp3') | ||
560 | expect(data.video.file.extensions).to.contain('.ogg') | ||
561 | expect(data.video.file.extensions).to.contain('.flac') | ||
562 | |||
563 | await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.OK_200 }) | ||
564 | await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.OK_200 }) | ||
565 | }) | ||
566 | |||
567 | it('Should have the configuration updated after a restart', async function () { | ||
568 | this.timeout(30000) | ||
569 | |||
570 | await killallServers([ server ]) | ||
571 | |||
572 | await server.run() | ||
573 | |||
574 | const data = await server.config.getCustomConfig() | ||
575 | |||
576 | checkUpdatedConfig(data) | ||
577 | }) | ||
578 | |||
579 | it('Should fetch the about information', async function () { | ||
580 | const data = await server.config.getAbout() | ||
581 | |||
582 | expect(data.instance.name).to.equal('PeerTube updated') | ||
583 | expect(data.instance.shortDescription).to.equal('my short description') | ||
584 | expect(data.instance.description).to.equal('my super description') | ||
585 | expect(data.instance.terms).to.equal('my super terms') | ||
586 | expect(data.instance.codeOfConduct).to.equal('my super coc') | ||
587 | |||
588 | expect(data.instance.creationReason).to.equal('my super creation reason') | ||
589 | expect(data.instance.moderationInformation).to.equal('my super moderation information') | ||
590 | expect(data.instance.administrator).to.equal('Kuja') | ||
591 | expect(data.instance.maintenanceLifetime).to.equal('forever') | ||
592 | expect(data.instance.businessModel).to.equal('my super business model') | ||
593 | expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM') | ||
594 | |||
595 | expect(data.instance.languages).to.deep.equal([ 'en', 'es' ]) | ||
596 | expect(data.instance.categories).to.deep.equal([ 1, 2 ]) | ||
597 | }) | ||
598 | |||
599 | it('Should remove the custom configuration', async function () { | ||
600 | await server.config.deleteCustomConfig() | ||
601 | |||
602 | const data = await server.config.getCustomConfig() | ||
603 | checkInitialConfig(server, data) | ||
604 | }) | ||
605 | |||
606 | it('Should enable/disable security headers', async function () { | ||
607 | this.timeout(25000) | ||
608 | |||
609 | { | ||
610 | const res = await makeGetRequest({ | ||
611 | url: server.url, | ||
612 | path: '/api/v1/config', | ||
613 | expectedStatus: 200 | ||
614 | }) | ||
615 | |||
616 | expect(res.headers['x-frame-options']).to.exist | ||
617 | expect(res.headers['x-powered-by']).to.equal('PeerTube') | ||
618 | } | ||
619 | |||
620 | await killallServers([ server ]) | ||
621 | |||
622 | const config = { | ||
623 | security: { | ||
624 | frameguard: { enabled: false }, | ||
625 | powered_by_header: { enabled: false } | ||
626 | } | ||
627 | } | ||
628 | await server.run(config) | ||
629 | |||
630 | { | ||
631 | const res = await makeGetRequest({ | ||
632 | url: server.url, | ||
633 | path: '/api/v1/config', | ||
634 | expectedStatus: 200 | ||
635 | }) | ||
636 | |||
637 | expect(res.headers['x-frame-options']).to.not.exist | ||
638 | expect(res.headers['x-powered-by']).to.not.exist | ||
639 | } | ||
640 | }) | ||
641 | |||
642 | after(async function () { | ||
643 | await cleanupTests([ server ]) | ||
644 | }) | ||
645 | }) | ||
diff --git a/packages/tests/src/api/server/contact-form.ts b/packages/tests/src/api/server/contact-form.ts new file mode 100644 index 000000000..03389aa64 --- /dev/null +++ b/packages/tests/src/api/server/contact-form.ts | |||
@@ -0,0 +1,101 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | ConfigCommand, | ||
10 | ContactFormCommand, | ||
11 | createSingleServer, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test contact form', function () { | ||
18 | let server: PeerTubeServer | ||
19 | const emails: object[] = [] | ||
20 | let command: ContactFormCommand | ||
21 | |||
22 | before(async function () { | ||
23 | this.timeout(30000) | ||
24 | |||
25 | const port = await MockSmtpServer.Instance.collectEmails(emails) | ||
26 | |||
27 | server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(port)) | ||
28 | await setAccessTokensToServers([ server ]) | ||
29 | |||
30 | command = server.contactForm | ||
31 | }) | ||
32 | |||
33 | it('Should send a contact form', async function () { | ||
34 | await command.send({ | ||
35 | fromEmail: 'toto@example.com', | ||
36 | body: 'my super message', | ||
37 | subject: 'my subject', | ||
38 | fromName: 'Super toto' | ||
39 | }) | ||
40 | |||
41 | await waitJobs(server) | ||
42 | |||
43 | expect(emails).to.have.lengthOf(1) | ||
44 | |||
45 | const email = emails[0] | ||
46 | |||
47 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
48 | expect(email['replyTo'][0]['address']).equal('toto@example.com') | ||
49 | expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com') | ||
50 | expect(email['subject']).contains('my subject') | ||
51 | expect(email['text']).contains('my super message') | ||
52 | }) | ||
53 | |||
54 | it('Should not have duplicated email address in text message', async function () { | ||
55 | const text = emails[0]['text'] as string | ||
56 | |||
57 | const matches = text.match(/toto@example.com/g) | ||
58 | expect(matches).to.have.lengthOf(1) | ||
59 | }) | ||
60 | |||
61 | it('Should not be able to send another contact form because of the anti spam checker', async function () { | ||
62 | await wait(1000) | ||
63 | |||
64 | await command.send({ | ||
65 | fromEmail: 'toto@example.com', | ||
66 | body: 'my super message', | ||
67 | subject: 'my subject', | ||
68 | fromName: 'Super toto' | ||
69 | }) | ||
70 | |||
71 | await command.send({ | ||
72 | fromEmail: 'toto@example.com', | ||
73 | body: 'my super message', | ||
74 | fromName: 'Super toto', | ||
75 | subject: 'my subject', | ||
76 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
77 | }) | ||
78 | }) | ||
79 | |||
80 | it('Should be able to send another contact form after a while', async function () { | ||
81 | await wait(1000) | ||
82 | |||
83 | await command.send({ | ||
84 | fromEmail: 'toto@example.com', | ||
85 | fromName: 'Super toto', | ||
86 | subject: 'my subject', | ||
87 | body: 'my super message' | ||
88 | }) | ||
89 | }) | ||
90 | |||
91 | it('Should not have the manage preferences link in the email', async function () { | ||
92 | const email = emails[0] | ||
93 | expect(email['text']).to.not.contain('Manage your notification preferences') | ||
94 | }) | ||
95 | |||
96 | after(async function () { | ||
97 | MockSmtpServer.Instance.kill() | ||
98 | |||
99 | await cleanupTests([ server ]) | ||
100 | }) | ||
101 | }) | ||
diff --git a/packages/tests/src/api/server/email.ts b/packages/tests/src/api/server/email.ts new file mode 100644 index 000000000..6d3f3f3bb --- /dev/null +++ b/packages/tests/src/api/server/email.ts | |||
@@ -0,0 +1,371 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test emails', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let userId: number | ||
18 | let userId2: number | ||
19 | let userAccessToken: string | ||
20 | |||
21 | let videoShortUUID: string | ||
22 | let videoId: number | ||
23 | |||
24 | let videoUserUUID: string | ||
25 | |||
26 | let verificationString: string | ||
27 | let verificationString2: string | ||
28 | |||
29 | const emails: object[] = [] | ||
30 | const user = { | ||
31 | username: 'user_1', | ||
32 | password: 'super_password' | ||
33 | } | ||
34 | |||
35 | before(async function () { | ||
36 | this.timeout(120000) | ||
37 | |||
38 | const emailPort = await MockSmtpServer.Instance.collectEmails(emails) | ||
39 | server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(emailPort)) | ||
40 | |||
41 | await setAccessTokensToServers([ server ]) | ||
42 | await server.config.enableSignup(true) | ||
43 | |||
44 | { | ||
45 | const created = await server.users.create({ username: user.username, password: user.password }) | ||
46 | userId = created.id | ||
47 | |||
48 | userAccessToken = await server.login.getAccessToken(user) | ||
49 | } | ||
50 | |||
51 | { | ||
52 | const attributes = { name: 'my super user video' } | ||
53 | const { uuid } = await server.videos.upload({ token: userAccessToken, attributes }) | ||
54 | videoUserUUID = uuid | ||
55 | } | ||
56 | |||
57 | { | ||
58 | const attributes = { | ||
59 | name: 'my super name' | ||
60 | } | ||
61 | const { shortUUID, id } = await server.videos.upload({ attributes }) | ||
62 | videoShortUUID = shortUUID | ||
63 | videoId = id | ||
64 | } | ||
65 | }) | ||
66 | |||
67 | describe('When resetting user password', function () { | ||
68 | |||
69 | it('Should ask to reset the password', async function () { | ||
70 | await server.users.askResetPassword({ email: 'user_1@example.com' }) | ||
71 | |||
72 | await waitJobs(server) | ||
73 | expect(emails).to.have.lengthOf(1) | ||
74 | |||
75 | const email = emails[0] | ||
76 | |||
77 | expect(email['from'][0]['name']).equal('PeerTube') | ||
78 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
79 | expect(email['to'][0]['address']).equal('user_1@example.com') | ||
80 | expect(email['subject']).contains('password') | ||
81 | |||
82 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
83 | expect(verificationStringMatches).not.to.be.null | ||
84 | |||
85 | verificationString = verificationStringMatches[1] | ||
86 | expect(verificationString).to.have.length.above(2) | ||
87 | |||
88 | const userIdMatches = /userId=([0-9]+)/.exec(email['text']) | ||
89 | expect(userIdMatches).not.to.be.null | ||
90 | |||
91 | userId = parseInt(userIdMatches[1], 10) | ||
92 | expect(verificationString).to.not.be.undefined | ||
93 | }) | ||
94 | |||
95 | it('Should not reset the password with an invalid verification string', async function () { | ||
96 | await server.users.resetPassword({ | ||
97 | userId, | ||
98 | verificationString: verificationString + 'b', | ||
99 | password: 'super_password2', | ||
100 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
101 | }) | ||
102 | }) | ||
103 | |||
104 | it('Should reset the password', async function () { | ||
105 | await server.users.resetPassword({ userId, verificationString, password: 'super_password2' }) | ||
106 | }) | ||
107 | |||
108 | it('Should not reset the password with the same verification string', async function () { | ||
109 | await server.users.resetPassword({ | ||
110 | userId, | ||
111 | verificationString, | ||
112 | password: 'super_password3', | ||
113 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
114 | }) | ||
115 | }) | ||
116 | |||
117 | it('Should login with this new password', async function () { | ||
118 | user.password = 'super_password2' | ||
119 | |||
120 | await server.login.getAccessToken(user) | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | describe('When creating a user without password', function () { | ||
125 | |||
126 | it('Should send a create password email', async function () { | ||
127 | await server.users.create({ username: 'create_password', password: '' }) | ||
128 | |||
129 | await waitJobs(server) | ||
130 | expect(emails).to.have.lengthOf(2) | ||
131 | |||
132 | const email = emails[1] | ||
133 | |||
134 | expect(email['from'][0]['name']).equal('PeerTube') | ||
135 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
136 | expect(email['to'][0]['address']).equal('create_password@example.com') | ||
137 | expect(email['subject']).contains('account') | ||
138 | expect(email['subject']).contains('password') | ||
139 | |||
140 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
141 | expect(verificationStringMatches).not.to.be.null | ||
142 | |||
143 | verificationString2 = verificationStringMatches[1] | ||
144 | expect(verificationString2).to.have.length.above(2) | ||
145 | |||
146 | const userIdMatches = /userId=([0-9]+)/.exec(email['text']) | ||
147 | expect(userIdMatches).not.to.be.null | ||
148 | |||
149 | userId2 = parseInt(userIdMatches[1], 10) | ||
150 | }) | ||
151 | |||
152 | it('Should not reset the password with an invalid verification string', async function () { | ||
153 | await server.users.resetPassword({ | ||
154 | userId: userId2, | ||
155 | verificationString: verificationString2 + 'c', | ||
156 | password: 'newly_created_password', | ||
157 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
158 | }) | ||
159 | }) | ||
160 | |||
161 | it('Should reset the password', async function () { | ||
162 | await server.users.resetPassword({ | ||
163 | userId: userId2, | ||
164 | verificationString: verificationString2, | ||
165 | password: 'newly_created_password' | ||
166 | }) | ||
167 | }) | ||
168 | |||
169 | it('Should login with this new password', async function () { | ||
170 | await server.login.getAccessToken({ | ||
171 | username: 'create_password', | ||
172 | password: 'newly_created_password' | ||
173 | }) | ||
174 | }) | ||
175 | }) | ||
176 | |||
177 | describe('When creating an abuse', function () { | ||
178 | |||
179 | it('Should send the notification email', async function () { | ||
180 | const reason = 'my super bad reason' | ||
181 | await server.abuses.report({ token: userAccessToken, videoId, reason }) | ||
182 | |||
183 | await waitJobs(server) | ||
184 | expect(emails).to.have.lengthOf(3) | ||
185 | |||
186 | const email = emails[2] | ||
187 | |||
188 | expect(email['from'][0]['name']).equal('PeerTube') | ||
189 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
190 | expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com') | ||
191 | expect(email['subject']).contains('abuse') | ||
192 | expect(email['text']).contains(videoShortUUID) | ||
193 | }) | ||
194 | }) | ||
195 | |||
196 | describe('When blocking/unblocking user', function () { | ||
197 | |||
198 | it('Should send the notification email when blocking a user', async function () { | ||
199 | const reason = 'my super bad reason' | ||
200 | await server.users.banUser({ userId, reason }) | ||
201 | |||
202 | await waitJobs(server) | ||
203 | expect(emails).to.have.lengthOf(4) | ||
204 | |||
205 | const email = emails[3] | ||
206 | |||
207 | expect(email['from'][0]['name']).equal('PeerTube') | ||
208 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
209 | expect(email['to'][0]['address']).equal('user_1@example.com') | ||
210 | expect(email['subject']).contains(' blocked') | ||
211 | expect(email['text']).contains(' blocked') | ||
212 | expect(email['text']).contains('bad reason') | ||
213 | }) | ||
214 | |||
215 | it('Should send the notification email when unblocking a user', async function () { | ||
216 | await server.users.unbanUser({ userId }) | ||
217 | |||
218 | await waitJobs(server) | ||
219 | expect(emails).to.have.lengthOf(5) | ||
220 | |||
221 | const email = emails[4] | ||
222 | |||
223 | expect(email['from'][0]['name']).equal('PeerTube') | ||
224 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
225 | expect(email['to'][0]['address']).equal('user_1@example.com') | ||
226 | expect(email['subject']).contains(' unblocked') | ||
227 | expect(email['text']).contains(' unblocked') | ||
228 | }) | ||
229 | }) | ||
230 | |||
231 | describe('When blacklisting a video', function () { | ||
232 | it('Should send the notification email', async function () { | ||
233 | const reason = 'my super reason' | ||
234 | await server.blacklist.add({ videoId: videoUserUUID, reason }) | ||
235 | |||
236 | await waitJobs(server) | ||
237 | expect(emails).to.have.lengthOf(6) | ||
238 | |||
239 | const email = emails[5] | ||
240 | |||
241 | expect(email['from'][0]['name']).equal('PeerTube') | ||
242 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
243 | expect(email['to'][0]['address']).equal('user_1@example.com') | ||
244 | expect(email['subject']).contains(' blacklisted') | ||
245 | expect(email['text']).contains('my super user video') | ||
246 | expect(email['text']).contains('my super reason') | ||
247 | }) | ||
248 | |||
249 | it('Should send the notification email', async function () { | ||
250 | await server.blacklist.remove({ videoId: videoUserUUID }) | ||
251 | |||
252 | await waitJobs(server) | ||
253 | expect(emails).to.have.lengthOf(7) | ||
254 | |||
255 | const email = emails[6] | ||
256 | |||
257 | expect(email['from'][0]['name']).equal('PeerTube') | ||
258 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
259 | expect(email['to'][0]['address']).equal('user_1@example.com') | ||
260 | expect(email['subject']).contains(' unblacklisted') | ||
261 | expect(email['text']).contains('my super user video') | ||
262 | }) | ||
263 | |||
264 | it('Should have the manage preferences link in the email', async function () { | ||
265 | const email = emails[6] | ||
266 | expect(email['text']).to.contain('Manage your notification preferences') | ||
267 | }) | ||
268 | }) | ||
269 | |||
270 | describe('When verifying a user email', function () { | ||
271 | |||
272 | it('Should ask to send the verification email', async function () { | ||
273 | await server.users.askSendVerifyEmail({ email: 'user_1@example.com' }) | ||
274 | |||
275 | await waitJobs(server) | ||
276 | expect(emails).to.have.lengthOf(8) | ||
277 | |||
278 | const email = emails[7] | ||
279 | |||
280 | expect(email['from'][0]['name']).equal('PeerTube') | ||
281 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
282 | expect(email['to'][0]['address']).equal('user_1@example.com') | ||
283 | expect(email['subject']).contains('Verify') | ||
284 | |||
285 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
286 | expect(verificationStringMatches).not.to.be.null | ||
287 | |||
288 | verificationString = verificationStringMatches[1] | ||
289 | expect(verificationString).to.not.be.undefined | ||
290 | expect(verificationString).to.have.length.above(2) | ||
291 | |||
292 | const userIdMatches = /userId=([0-9]+)/.exec(email['text']) | ||
293 | expect(userIdMatches).not.to.be.null | ||
294 | |||
295 | userId = parseInt(userIdMatches[1], 10) | ||
296 | }) | ||
297 | |||
298 | it('Should not verify the email with an invalid verification string', async function () { | ||
299 | await server.users.verifyEmail({ | ||
300 | userId, | ||
301 | verificationString: verificationString + 'b', | ||
302 | isPendingEmail: false, | ||
303 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
304 | }) | ||
305 | }) | ||
306 | |||
307 | it('Should verify the email', async function () { | ||
308 | await server.users.verifyEmail({ userId, verificationString }) | ||
309 | }) | ||
310 | }) | ||
311 | |||
312 | describe('When verifying a registration email', function () { | ||
313 | let registrationId: number | ||
314 | let registrationIdEmail: number | ||
315 | |||
316 | before(async function () { | ||
317 | const { id } = await server.registrations.requestRegistration({ | ||
318 | username: 'request_1', | ||
319 | email: 'request_1@example.com', | ||
320 | registrationReason: 'tt' | ||
321 | }) | ||
322 | registrationId = id | ||
323 | }) | ||
324 | |||
325 | it('Should ask to send the verification email', async function () { | ||
326 | await server.registrations.askSendVerifyEmail({ email: 'request_1@example.com' }) | ||
327 | |||
328 | await waitJobs(server) | ||
329 | expect(emails).to.have.lengthOf(9) | ||
330 | |||
331 | const email = emails[8] | ||
332 | |||
333 | expect(email['from'][0]['name']).equal('PeerTube') | ||
334 | expect(email['from'][0]['address']).equal('test-admin@127.0.0.1') | ||
335 | expect(email['to'][0]['address']).equal('request_1@example.com') | ||
336 | expect(email['subject']).contains('Verify') | ||
337 | |||
338 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
339 | expect(verificationStringMatches).not.to.be.null | ||
340 | |||
341 | verificationString = verificationStringMatches[1] | ||
342 | expect(verificationString).to.not.be.undefined | ||
343 | expect(verificationString).to.have.length.above(2) | ||
344 | |||
345 | const registrationIdMatches = /registrationId=([0-9]+)/.exec(email['text']) | ||
346 | expect(registrationIdMatches).not.to.be.null | ||
347 | |||
348 | registrationIdEmail = parseInt(registrationIdMatches[1], 10) | ||
349 | |||
350 | expect(registrationId).to.equal(registrationIdEmail) | ||
351 | }) | ||
352 | |||
353 | it('Should not verify the email with an invalid verification string', async function () { | ||
354 | await server.registrations.verifyEmail({ | ||
355 | registrationId: registrationIdEmail, | ||
356 | verificationString: verificationString + 'b', | ||
357 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
358 | }) | ||
359 | }) | ||
360 | |||
361 | it('Should verify the email', async function () { | ||
362 | await server.registrations.verifyEmail({ registrationId: registrationIdEmail, verificationString }) | ||
363 | }) | ||
364 | }) | ||
365 | |||
366 | after(async function () { | ||
367 | MockSmtpServer.Instance.kill() | ||
368 | |||
369 | await cleanupTests([ server ]) | ||
370 | }) | ||
371 | }) | ||
diff --git a/packages/tests/src/api/server/follow-constraints.ts b/packages/tests/src/api/server/follow-constraints.ts new file mode 100644 index 000000000..8d277c906 --- /dev/null +++ b/packages/tests/src/api/server/follow-constraints.ts | |||
@@ -0,0 +1,321 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode, PeerTubeProblemDocument, ServerErrorCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test follow constraints', function () { | ||
15 | let servers: PeerTubeServer[] = [] | ||
16 | let video1UUID: string | ||
17 | let video2UUID: string | ||
18 | let userToken: string | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(240000) | ||
22 | |||
23 | servers = await createMultipleServers(2) | ||
24 | |||
25 | // Get the access tokens | ||
26 | await setAccessTokensToServers(servers) | ||
27 | |||
28 | { | ||
29 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video server 1' } }) | ||
30 | video1UUID = uuid | ||
31 | } | ||
32 | { | ||
33 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video server 2' } }) | ||
34 | video2UUID = uuid | ||
35 | } | ||
36 | |||
37 | const user = { | ||
38 | username: 'user1', | ||
39 | password: 'super_password' | ||
40 | } | ||
41 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
42 | userToken = await servers[0].login.getAccessToken(user) | ||
43 | |||
44 | await doubleFollow(servers[0], servers[1]) | ||
45 | }) | ||
46 | |||
47 | describe('With a followed instance', function () { | ||
48 | |||
49 | describe('With an unlogged user', function () { | ||
50 | |||
51 | it('Should get the local video', async function () { | ||
52 | await servers[0].videos.get({ id: video1UUID }) | ||
53 | }) | ||
54 | |||
55 | it('Should get the remote video', async function () { | ||
56 | await servers[0].videos.get({ id: video2UUID }) | ||
57 | }) | ||
58 | |||
59 | it('Should list local account videos', async function () { | ||
60 | const { total, data } = await servers[0].videos.listByAccount({ handle: 'root@' + servers[0].host }) | ||
61 | |||
62 | expect(total).to.equal(1) | ||
63 | expect(data).to.have.lengthOf(1) | ||
64 | }) | ||
65 | |||
66 | it('Should list remote account videos', async function () { | ||
67 | const { total, data } = await servers[0].videos.listByAccount({ handle: 'root@' + servers[1].host }) | ||
68 | |||
69 | expect(total).to.equal(1) | ||
70 | expect(data).to.have.lengthOf(1) | ||
71 | }) | ||
72 | |||
73 | it('Should list local channel videos', async function () { | ||
74 | const handle = 'root_channel@' + servers[0].host | ||
75 | const { total, data } = await servers[0].videos.listByChannel({ handle }) | ||
76 | |||
77 | expect(total).to.equal(1) | ||
78 | expect(data).to.have.lengthOf(1) | ||
79 | }) | ||
80 | |||
81 | it('Should list remote channel videos', async function () { | ||
82 | const handle = 'root_channel@' + servers[1].host | ||
83 | const { total, data } = await servers[0].videos.listByChannel({ handle }) | ||
84 | |||
85 | expect(total).to.equal(1) | ||
86 | expect(data).to.have.lengthOf(1) | ||
87 | }) | ||
88 | }) | ||
89 | |||
90 | describe('With a logged user', function () { | ||
91 | it('Should get the local video', async function () { | ||
92 | await servers[0].videos.getWithToken({ token: userToken, id: video1UUID }) | ||
93 | }) | ||
94 | |||
95 | it('Should get the remote video', async function () { | ||
96 | await servers[0].videos.getWithToken({ token: userToken, id: video2UUID }) | ||
97 | }) | ||
98 | |||
99 | it('Should list local account videos', async function () { | ||
100 | const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[0].host }) | ||
101 | |||
102 | expect(total).to.equal(1) | ||
103 | expect(data).to.have.lengthOf(1) | ||
104 | }) | ||
105 | |||
106 | it('Should list remote account videos', async function () { | ||
107 | const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[1].host }) | ||
108 | |||
109 | expect(total).to.equal(1) | ||
110 | expect(data).to.have.lengthOf(1) | ||
111 | }) | ||
112 | |||
113 | it('Should list local channel videos', async function () { | ||
114 | const handle = 'root_channel@' + servers[0].host | ||
115 | const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle }) | ||
116 | |||
117 | expect(total).to.equal(1) | ||
118 | expect(data).to.have.lengthOf(1) | ||
119 | }) | ||
120 | |||
121 | it('Should list remote channel videos', async function () { | ||
122 | const handle = 'root_channel@' + servers[1].host | ||
123 | const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle }) | ||
124 | |||
125 | expect(total).to.equal(1) | ||
126 | expect(data).to.have.lengthOf(1) | ||
127 | }) | ||
128 | }) | ||
129 | }) | ||
130 | |||
131 | describe('With a non followed instance', function () { | ||
132 | |||
133 | before(async function () { | ||
134 | this.timeout(30000) | ||
135 | |||
136 | await servers[0].follows.unfollow({ target: servers[1] }) | ||
137 | }) | ||
138 | |||
139 | describe('With an unlogged user', function () { | ||
140 | |||
141 | it('Should get the local video', async function () { | ||
142 | await servers[0].videos.get({ id: video1UUID }) | ||
143 | }) | ||
144 | |||
145 | it('Should not get the remote video', async function () { | ||
146 | const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
147 | const error = body as unknown as PeerTubeProblemDocument | ||
148 | |||
149 | const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints' | ||
150 | expect(error.type).to.equal(doc) | ||
151 | expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS) | ||
152 | |||
153 | expect(error.detail).to.equal('Cannot get this video regarding follow constraints') | ||
154 | expect(error.error).to.equal(error.detail) | ||
155 | |||
156 | expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
157 | |||
158 | expect(error.originUrl).to.contains(servers[1].url) | ||
159 | }) | ||
160 | |||
161 | it('Should list local account videos', async function () { | ||
162 | const { total, data } = await servers[0].videos.listByAccount({ | ||
163 | token: null, | ||
164 | handle: 'root@' + servers[0].host | ||
165 | }) | ||
166 | |||
167 | expect(total).to.equal(1) | ||
168 | expect(data).to.have.lengthOf(1) | ||
169 | }) | ||
170 | |||
171 | it('Should not list remote account videos', async function () { | ||
172 | const { total, data } = await servers[0].videos.listByAccount({ | ||
173 | token: null, | ||
174 | handle: 'root@' + servers[1].host | ||
175 | }) | ||
176 | |||
177 | expect(total).to.equal(0) | ||
178 | expect(data).to.have.lengthOf(0) | ||
179 | }) | ||
180 | |||
181 | it('Should list local channel videos', async function () { | ||
182 | const handle = 'root_channel@' + servers[0].host | ||
183 | const { total, data } = await servers[0].videos.listByChannel({ token: null, handle }) | ||
184 | |||
185 | expect(total).to.equal(1) | ||
186 | expect(data).to.have.lengthOf(1) | ||
187 | }) | ||
188 | |||
189 | it('Should not list remote channel videos', async function () { | ||
190 | const handle = 'root_channel@' + servers[1].host | ||
191 | const { total, data } = await servers[0].videos.listByChannel({ token: null, handle }) | ||
192 | |||
193 | expect(total).to.equal(0) | ||
194 | expect(data).to.have.lengthOf(0) | ||
195 | }) | ||
196 | }) | ||
197 | |||
198 | describe('With a logged user', function () { | ||
199 | |||
200 | it('Should get the local video', async function () { | ||
201 | await servers[0].videos.getWithToken({ token: userToken, id: video1UUID }) | ||
202 | }) | ||
203 | |||
204 | it('Should get the remote video', async function () { | ||
205 | await servers[0].videos.getWithToken({ token: userToken, id: video2UUID }) | ||
206 | }) | ||
207 | |||
208 | it('Should list local account videos', async function () { | ||
209 | const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[0].host }) | ||
210 | |||
211 | expect(total).to.equal(1) | ||
212 | expect(data).to.have.lengthOf(1) | ||
213 | }) | ||
214 | |||
215 | it('Should list remote account videos', async function () { | ||
216 | const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[1].host }) | ||
217 | |||
218 | expect(total).to.equal(1) | ||
219 | expect(data).to.have.lengthOf(1) | ||
220 | }) | ||
221 | |||
222 | it('Should list local channel videos', async function () { | ||
223 | const handle = 'root_channel@' + servers[0].host | ||
224 | const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle }) | ||
225 | |||
226 | expect(total).to.equal(1) | ||
227 | expect(data).to.have.lengthOf(1) | ||
228 | }) | ||
229 | |||
230 | it('Should list remote channel videos', async function () { | ||
231 | const handle = 'root_channel@' + servers[1].host | ||
232 | const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle }) | ||
233 | |||
234 | expect(total).to.equal(1) | ||
235 | expect(data).to.have.lengthOf(1) | ||
236 | }) | ||
237 | }) | ||
238 | }) | ||
239 | |||
240 | describe('When following a remote account', function () { | ||
241 | |||
242 | before(async function () { | ||
243 | this.timeout(60000) | ||
244 | |||
245 | await servers[0].follows.follow({ handles: [ 'root@' + servers[1].host ] }) | ||
246 | await waitJobs(servers) | ||
247 | }) | ||
248 | |||
249 | it('Should get the remote video with an unlogged user', async function () { | ||
250 | await servers[0].videos.get({ id: video2UUID }) | ||
251 | }) | ||
252 | |||
253 | it('Should get the remote video with a logged in user', async function () { | ||
254 | await servers[0].videos.getWithToken({ token: userToken, id: video2UUID }) | ||
255 | }) | ||
256 | }) | ||
257 | |||
258 | describe('When unfollowing a remote account', function () { | ||
259 | |||
260 | before(async function () { | ||
261 | this.timeout(60000) | ||
262 | |||
263 | await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) | ||
264 | await waitJobs(servers) | ||
265 | }) | ||
266 | |||
267 | it('Should not get the remote video with an unlogged user', async function () { | ||
268 | const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
269 | |||
270 | const error = body as unknown as PeerTubeProblemDocument | ||
271 | expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS) | ||
272 | }) | ||
273 | |||
274 | it('Should get the remote video with a logged in user', async function () { | ||
275 | await servers[0].videos.getWithToken({ token: userToken, id: video2UUID }) | ||
276 | }) | ||
277 | }) | ||
278 | |||
279 | describe('When following a remote channel', function () { | ||
280 | |||
281 | before(async function () { | ||
282 | this.timeout(60000) | ||
283 | |||
284 | await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[1].host ] }) | ||
285 | await waitJobs(servers) | ||
286 | }) | ||
287 | |||
288 | it('Should get the remote video with an unlogged user', async function () { | ||
289 | await servers[0].videos.get({ id: video2UUID }) | ||
290 | }) | ||
291 | |||
292 | it('Should get the remote video with a logged in user', async function () { | ||
293 | await servers[0].videos.getWithToken({ token: userToken, id: video2UUID }) | ||
294 | }) | ||
295 | }) | ||
296 | |||
297 | describe('When unfollowing a remote channel', function () { | ||
298 | |||
299 | before(async function () { | ||
300 | this.timeout(60000) | ||
301 | |||
302 | await servers[0].follows.unfollow({ target: 'root_channel@' + servers[1].host }) | ||
303 | await waitJobs(servers) | ||
304 | }) | ||
305 | |||
306 | it('Should not get the remote video with an unlogged user', async function () { | ||
307 | const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
308 | |||
309 | const error = body as unknown as PeerTubeProblemDocument | ||
310 | expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS) | ||
311 | }) | ||
312 | |||
313 | it('Should get the remote video with a logged in user', async function () { | ||
314 | await servers[0].videos.getWithToken({ token: userToken, id: video2UUID }) | ||
315 | }) | ||
316 | }) | ||
317 | |||
318 | after(async function () { | ||
319 | await cleanupTests(servers) | ||
320 | }) | ||
321 | }) | ||
diff --git a/packages/tests/src/api/server/follows-moderation.ts b/packages/tests/src/api/server/follows-moderation.ts new file mode 100644 index 000000000..811dd5c22 --- /dev/null +++ b/packages/tests/src/api/server/follows-moderation.ts | |||
@@ -0,0 +1,364 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { expectStartWith } from '@tests/shared/checks.js' | ||
5 | import { ActorFollow, FollowState } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | FollowsCommand, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | async function checkServer1And2HasFollowers (servers: PeerTubeServer[], state = 'accepted') { | ||
16 | const fns = [ | ||
17 | servers[0].follows.getFollowings.bind(servers[0].follows), | ||
18 | servers[1].follows.getFollowers.bind(servers[1].follows) | ||
19 | ] | ||
20 | |||
21 | for (const fn of fns) { | ||
22 | const body = await fn({ start: 0, count: 5, sort: 'createdAt' }) | ||
23 | expect(body.total).to.equal(1) | ||
24 | |||
25 | const follow = body.data[0] | ||
26 | expect(follow.state).to.equal(state) | ||
27 | expect(follow.follower.url).to.equal(servers[0].url + '/accounts/peertube') | ||
28 | expect(follow.following.url).to.equal(servers[1].url + '/accounts/peertube') | ||
29 | } | ||
30 | } | ||
31 | |||
32 | async function checkFollows (options: { | ||
33 | follower: PeerTubeServer | ||
34 | followerState: FollowState | 'deleted' | ||
35 | |||
36 | following: PeerTubeServer | ||
37 | followingState: FollowState | 'deleted' | ||
38 | }) { | ||
39 | const { follower, followerState, followingState, following } = options | ||
40 | |||
41 | const followerUrl = follower.url + '/accounts/peertube' | ||
42 | const followingUrl = following.url + '/accounts/peertube' | ||
43 | const finder = (d: ActorFollow) => d.follower.url === followerUrl && d.following.url === followingUrl | ||
44 | |||
45 | { | ||
46 | const { data } = await follower.follows.getFollowings() | ||
47 | const follow = data.find(finder) | ||
48 | |||
49 | if (followerState === 'deleted') { | ||
50 | expect(follow).to.not.exist | ||
51 | } else { | ||
52 | expect(follow.state).to.equal(followerState) | ||
53 | expect(follow.follower.url).to.equal(followerUrl) | ||
54 | expect(follow.following.url).to.equal(followingUrl) | ||
55 | } | ||
56 | } | ||
57 | |||
58 | { | ||
59 | const { data } = await following.follows.getFollowers() | ||
60 | const follow = data.find(finder) | ||
61 | |||
62 | if (followingState === 'deleted') { | ||
63 | expect(follow).to.not.exist | ||
64 | } else { | ||
65 | expect(follow.state).to.equal(followingState) | ||
66 | expect(follow.follower.url).to.equal(followerUrl) | ||
67 | expect(follow.following.url).to.equal(followingUrl) | ||
68 | } | ||
69 | } | ||
70 | } | ||
71 | |||
72 | async function checkNoFollowers (servers: PeerTubeServer[]) { | ||
73 | const fns = [ | ||
74 | servers[0].follows.getFollowings.bind(servers[0].follows), | ||
75 | servers[1].follows.getFollowers.bind(servers[1].follows) | ||
76 | ] | ||
77 | |||
78 | for (const fn of fns) { | ||
79 | const body = await fn({ start: 0, count: 5, sort: 'createdAt', state: 'accepted' }) | ||
80 | expect(body.total).to.equal(0) | ||
81 | } | ||
82 | } | ||
83 | |||
84 | describe('Test follows moderation', function () { | ||
85 | let servers: PeerTubeServer[] = [] | ||
86 | let commands: FollowsCommand[] | ||
87 | |||
88 | before(async function () { | ||
89 | this.timeout(240000) | ||
90 | |||
91 | servers = await createMultipleServers(3) | ||
92 | |||
93 | // Get the access tokens | ||
94 | await setAccessTokensToServers(servers) | ||
95 | |||
96 | commands = servers.map(s => s.follows) | ||
97 | }) | ||
98 | |||
99 | describe('Default behaviour', function () { | ||
100 | |||
101 | it('Should have server 1 following server 2', async function () { | ||
102 | this.timeout(30000) | ||
103 | |||
104 | await commands[0].follow({ hosts: [ servers[1].url ] }) | ||
105 | |||
106 | await waitJobs(servers) | ||
107 | }) | ||
108 | |||
109 | it('Should have correct follows', async function () { | ||
110 | await checkServer1And2HasFollowers(servers) | ||
111 | }) | ||
112 | |||
113 | it('Should remove follower on server 2', async function () { | ||
114 | await commands[1].removeFollower({ follower: servers[0] }) | ||
115 | |||
116 | await waitJobs(servers) | ||
117 | }) | ||
118 | |||
119 | it('Should not not have follows anymore', async function () { | ||
120 | await checkNoFollowers(servers) | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | describe('Disabled/Enabled followers', function () { | ||
125 | |||
126 | it('Should disable followers on server 2', async function () { | ||
127 | const subConfig = { | ||
128 | followers: { | ||
129 | instance: { | ||
130 | enabled: false, | ||
131 | manualApproval: false | ||
132 | } | ||
133 | } | ||
134 | } | ||
135 | |||
136 | await servers[1].config.updateCustomSubConfig({ newConfig: subConfig }) | ||
137 | |||
138 | await commands[0].follow({ hosts: [ servers[1].url ] }) | ||
139 | await waitJobs(servers) | ||
140 | |||
141 | await checkNoFollowers(servers) | ||
142 | }) | ||
143 | |||
144 | it('Should re enable followers on server 2', async function () { | ||
145 | const subConfig = { | ||
146 | followers: { | ||
147 | instance: { | ||
148 | enabled: true, | ||
149 | manualApproval: false | ||
150 | } | ||
151 | } | ||
152 | } | ||
153 | |||
154 | await servers[1].config.updateCustomSubConfig({ newConfig: subConfig }) | ||
155 | |||
156 | await commands[0].follow({ hosts: [ servers[1].url ] }) | ||
157 | await waitJobs(servers) | ||
158 | |||
159 | await checkServer1And2HasFollowers(servers) | ||
160 | }) | ||
161 | }) | ||
162 | |||
163 | describe('Manual approbation', function () { | ||
164 | |||
165 | it('Should manually approve followers', async function () { | ||
166 | this.timeout(20000) | ||
167 | |||
168 | await commands[0].unfollow({ target: servers[1] }) | ||
169 | await waitJobs(servers) | ||
170 | |||
171 | const subConfig = { | ||
172 | followers: { | ||
173 | instance: { | ||
174 | enabled: true, | ||
175 | manualApproval: true | ||
176 | } | ||
177 | } | ||
178 | } | ||
179 | |||
180 | await servers[1].config.updateCustomSubConfig({ newConfig: subConfig }) | ||
181 | await servers[2].config.updateCustomSubConfig({ newConfig: subConfig }) | ||
182 | |||
183 | await commands[0].follow({ hosts: [ servers[1].url ] }) | ||
184 | await waitJobs(servers) | ||
185 | |||
186 | await checkServer1And2HasFollowers(servers, 'pending') | ||
187 | }) | ||
188 | |||
189 | it('Should accept a follower', async function () { | ||
190 | await commands[1].acceptFollower({ follower: 'peertube@' + servers[0].host }) | ||
191 | await waitJobs(servers) | ||
192 | |||
193 | await checkServer1And2HasFollowers(servers) | ||
194 | }) | ||
195 | |||
196 | it('Should reject another follower', async function () { | ||
197 | this.timeout(20000) | ||
198 | |||
199 | await commands[0].follow({ hosts: [ servers[2].url ] }) | ||
200 | await waitJobs(servers) | ||
201 | |||
202 | { | ||
203 | const body = await commands[0].getFollowings() | ||
204 | expect(body.total).to.equal(2) | ||
205 | } | ||
206 | |||
207 | { | ||
208 | const body = await commands[1].getFollowers() | ||
209 | expect(body.total).to.equal(1) | ||
210 | } | ||
211 | |||
212 | { | ||
213 | const body = await commands[2].getFollowers() | ||
214 | expect(body.total).to.equal(1) | ||
215 | } | ||
216 | |||
217 | await commands[2].rejectFollower({ follower: 'peertube@' + servers[0].host }) | ||
218 | await waitJobs(servers) | ||
219 | |||
220 | { // server 1 | ||
221 | { | ||
222 | const { data } = await commands[0].getFollowings({ state: 'accepted' }) | ||
223 | expect(data).to.have.lengthOf(1) | ||
224 | } | ||
225 | |||
226 | { | ||
227 | const { data } = await commands[0].getFollowings({ state: 'rejected' }) | ||
228 | expect(data).to.have.lengthOf(1) | ||
229 | expectStartWith(data[0].following.url, servers[2].url) | ||
230 | } | ||
231 | } | ||
232 | |||
233 | { // server 3 | ||
234 | { | ||
235 | const { data } = await commands[2].getFollowers({ state: 'accepted' }) | ||
236 | expect(data).to.have.lengthOf(0) | ||
237 | } | ||
238 | |||
239 | { | ||
240 | const { data } = await commands[2].getFollowers({ state: 'rejected' }) | ||
241 | expect(data).to.have.lengthOf(1) | ||
242 | expectStartWith(data[0].follower.url, servers[0].url) | ||
243 | } | ||
244 | } | ||
245 | }) | ||
246 | |||
247 | it('Should still auto accept channel followers', async function () { | ||
248 | await commands[0].follow({ handles: [ 'root_channel@' + servers[1].host ] }) | ||
249 | |||
250 | await waitJobs(servers) | ||
251 | |||
252 | const body = await commands[0].getFollowings() | ||
253 | const follow = body.data[0] | ||
254 | expect(follow.following.name).to.equal('root_channel') | ||
255 | expect(follow.state).to.equal('accepted') | ||
256 | }) | ||
257 | }) | ||
258 | |||
259 | describe('Accept/reject state', function () { | ||
260 | |||
261 | it('Should not change the follow on refollow with and without auto accept', async function () { | ||
262 | const run = async () => { | ||
263 | await commands[0].follow({ hosts: [ servers[2].url ] }) | ||
264 | await waitJobs(servers) | ||
265 | |||
266 | await checkFollows({ | ||
267 | follower: servers[0], | ||
268 | followerState: 'rejected', | ||
269 | following: servers[2], | ||
270 | followingState: 'rejected' | ||
271 | }) | ||
272 | } | ||
273 | |||
274 | await servers[2].config.updateExistingSubConfig({ newConfig: { followers: { instance: { manualApproval: false } } } }) | ||
275 | await run() | ||
276 | |||
277 | await servers[2].config.updateExistingSubConfig({ newConfig: { followers: { instance: { manualApproval: true } } } }) | ||
278 | await run() | ||
279 | }) | ||
280 | |||
281 | it('Should not change the rejected status on unfollow', async function () { | ||
282 | await commands[0].unfollow({ target: servers[2] }) | ||
283 | await waitJobs(servers) | ||
284 | |||
285 | await checkFollows({ | ||
286 | follower: servers[0], | ||
287 | followerState: 'deleted', | ||
288 | following: servers[2], | ||
289 | followingState: 'rejected' | ||
290 | }) | ||
291 | }) | ||
292 | |||
293 | it('Should delete the follower and add again the follower', async function () { | ||
294 | await commands[2].removeFollower({ follower: servers[0] }) | ||
295 | await waitJobs(servers) | ||
296 | |||
297 | await commands[0].follow({ hosts: [ servers[2].url ] }) | ||
298 | await waitJobs(servers) | ||
299 | |||
300 | await checkFollows({ | ||
301 | follower: servers[0], | ||
302 | followerState: 'pending', | ||
303 | following: servers[2], | ||
304 | followingState: 'pending' | ||
305 | }) | ||
306 | }) | ||
307 | |||
308 | it('Should be able to reject a previously accepted follower', async function () { | ||
309 | await commands[1].rejectFollower({ follower: 'peertube@' + servers[0].host }) | ||
310 | await waitJobs(servers) | ||
311 | |||
312 | await checkFollows({ | ||
313 | follower: servers[0], | ||
314 | followerState: 'rejected', | ||
315 | following: servers[1], | ||
316 | followingState: 'rejected' | ||
317 | }) | ||
318 | }) | ||
319 | |||
320 | it('Should be able to re accept a previously rejected follower', async function () { | ||
321 | await commands[1].acceptFollower({ follower: 'peertube@' + servers[0].host }) | ||
322 | await waitJobs(servers) | ||
323 | |||
324 | await checkFollows({ | ||
325 | follower: servers[0], | ||
326 | followerState: 'accepted', | ||
327 | following: servers[1], | ||
328 | followingState: 'accepted' | ||
329 | }) | ||
330 | }) | ||
331 | }) | ||
332 | |||
333 | describe('Muted servers', function () { | ||
334 | |||
335 | it('Should ignore follow requests of muted servers', async function () { | ||
336 | await servers[1].blocklist.addToServerBlocklist({ server: servers[0].host }) | ||
337 | |||
338 | await commands[0].unfollow({ target: servers[1] }) | ||
339 | |||
340 | await waitJobs(servers) | ||
341 | |||
342 | await checkFollows({ | ||
343 | follower: servers[0], | ||
344 | followerState: 'deleted', | ||
345 | following: servers[1], | ||
346 | followingState: 'deleted' | ||
347 | }) | ||
348 | |||
349 | await commands[0].follow({ hosts: [ servers[1].host ] }) | ||
350 | await waitJobs(servers) | ||
351 | |||
352 | await checkFollows({ | ||
353 | follower: servers[0], | ||
354 | followerState: 'rejected', | ||
355 | following: servers[1], | ||
356 | followingState: 'deleted' | ||
357 | }) | ||
358 | }) | ||
359 | }) | ||
360 | |||
361 | after(async function () { | ||
362 | await cleanupTests(servers) | ||
363 | }) | ||
364 | }) | ||
diff --git a/packages/tests/src/api/server/follows.ts b/packages/tests/src/api/server/follows.ts new file mode 100644 index 000000000..fbe2e87da --- /dev/null +++ b/packages/tests/src/api/server/follows.ts | |||
@@ -0,0 +1,644 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { Video, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands' | ||
6 | import { expectAccountFollows, expectChannelsFollows } from '@tests/shared/actors.js' | ||
7 | import { testCaptionFile } from '@tests/shared/captions.js' | ||
8 | import { dateIsValid } from '@tests/shared/checks.js' | ||
9 | import { completeVideoCheck } from '@tests/shared/videos.js' | ||
10 | |||
11 | describe('Test follows', function () { | ||
12 | |||
13 | describe('Complex follow', function () { | ||
14 | let servers: PeerTubeServer[] = [] | ||
15 | |||
16 | before(async function () { | ||
17 | this.timeout(120000) | ||
18 | |||
19 | servers = await createMultipleServers(3) | ||
20 | |||
21 | // Get the access tokens | ||
22 | await setAccessTokensToServers(servers) | ||
23 | }) | ||
24 | |||
25 | describe('Data propagation after follow', function () { | ||
26 | |||
27 | it('Should not have followers/followings', async function () { | ||
28 | for (const server of servers) { | ||
29 | const bodies = await Promise.all([ | ||
30 | server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }), | ||
31 | server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) | ||
32 | ]) | ||
33 | |||
34 | for (const body of bodies) { | ||
35 | expect(body.total).to.equal(0) | ||
36 | |||
37 | const follows = body.data | ||
38 | expect(follows).to.be.an('array') | ||
39 | expect(follows).to.have.lengthOf(0) | ||
40 | } | ||
41 | } | ||
42 | }) | ||
43 | |||
44 | it('Should have server 1 following root account of server 2 and server 3', async function () { | ||
45 | this.timeout(30000) | ||
46 | |||
47 | await servers[0].follows.follow({ | ||
48 | hosts: [ servers[2].url ], | ||
49 | handles: [ 'root@' + servers[1].host ] | ||
50 | }) | ||
51 | |||
52 | await waitJobs(servers) | ||
53 | }) | ||
54 | |||
55 | it('Should have 2 followings on server 1', async function () { | ||
56 | const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' }) | ||
57 | expect(body.total).to.equal(2) | ||
58 | |||
59 | let follows = body.data | ||
60 | expect(follows).to.be.an('array') | ||
61 | expect(follows).to.have.lengthOf(1) | ||
62 | |||
63 | const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' }) | ||
64 | follows = follows.concat(body2.data) | ||
65 | |||
66 | const server2Follow = follows.find(f => f.following.host === servers[1].host) | ||
67 | const server3Follow = follows.find(f => f.following.host === servers[2].host) | ||
68 | |||
69 | expect(server2Follow).to.not.be.undefined | ||
70 | expect(server2Follow.following.name).to.equal('root') | ||
71 | expect(server2Follow.state).to.equal('accepted') | ||
72 | |||
73 | expect(server3Follow).to.not.be.undefined | ||
74 | expect(server3Follow.following.name).to.equal('peertube') | ||
75 | expect(server3Follow.state).to.equal('accepted') | ||
76 | }) | ||
77 | |||
78 | it('Should have 0 followings on server 2 and 3', async function () { | ||
79 | for (const server of [ servers[1], servers[2] ]) { | ||
80 | const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) | ||
81 | expect(body.total).to.equal(0) | ||
82 | |||
83 | const follows = body.data | ||
84 | expect(follows).to.be.an('array') | ||
85 | expect(follows).to.have.lengthOf(0) | ||
86 | } | ||
87 | }) | ||
88 | |||
89 | it('Should have 1 followers on server 3', async function () { | ||
90 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | ||
91 | expect(body.total).to.equal(1) | ||
92 | |||
93 | const follows = body.data | ||
94 | expect(follows).to.be.an('array') | ||
95 | expect(follows).to.have.lengthOf(1) | ||
96 | expect(follows[0].follower.host).to.equal(servers[0].host) | ||
97 | }) | ||
98 | |||
99 | it('Should have 0 followers on server 1 and 2', async function () { | ||
100 | for (const server of [ servers[0], servers[1] ]) { | ||
101 | const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }) | ||
102 | expect(body.total).to.equal(0) | ||
103 | |||
104 | const follows = body.data | ||
105 | expect(follows).to.be.an('array') | ||
106 | expect(follows).to.have.lengthOf(0) | ||
107 | } | ||
108 | }) | ||
109 | |||
110 | it('Should search/filter followings on server 1', async function () { | ||
111 | const sort = 'createdAt' | ||
112 | const start = 0 | ||
113 | const count = 1 | ||
114 | |||
115 | { | ||
116 | const search = ':' + servers[1].port | ||
117 | |||
118 | { | ||
119 | const body = await servers[0].follows.getFollowings({ start, count, sort, search }) | ||
120 | expect(body.total).to.equal(1) | ||
121 | |||
122 | const follows = body.data | ||
123 | expect(follows).to.have.lengthOf(1) | ||
124 | expect(follows[0].following.host).to.equal(servers[1].host) | ||
125 | } | ||
126 | |||
127 | { | ||
128 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' }) | ||
129 | expect(body.total).to.equal(1) | ||
130 | expect(body.data).to.have.lengthOf(1) | ||
131 | } | ||
132 | |||
133 | { | ||
134 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) | ||
135 | expect(body.total).to.equal(1) | ||
136 | expect(body.data).to.have.lengthOf(1) | ||
137 | } | ||
138 | |||
139 | { | ||
140 | const body = await servers[0].follows.getFollowings({ | ||
141 | start, | ||
142 | count, | ||
143 | sort, | ||
144 | search, | ||
145 | state: 'accepted', | ||
146 | actorType: 'Application' | ||
147 | }) | ||
148 | expect(body.total).to.equal(0) | ||
149 | expect(body.data).to.have.lengthOf(0) | ||
150 | } | ||
151 | |||
152 | { | ||
153 | const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' }) | ||
154 | expect(body.total).to.equal(0) | ||
155 | expect(body.data).to.have.lengthOf(0) | ||
156 | } | ||
157 | } | ||
158 | |||
159 | { | ||
160 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' }) | ||
161 | expect(body.total).to.equal(1) | ||
162 | expect(body.data).to.have.lengthOf(1) | ||
163 | } | ||
164 | |||
165 | { | ||
166 | const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' }) | ||
167 | expect(body.total).to.equal(0) | ||
168 | |||
169 | expect(body.data).to.have.lengthOf(0) | ||
170 | } | ||
171 | }) | ||
172 | |||
173 | it('Should search/filter followers on server 2', async function () { | ||
174 | const start = 0 | ||
175 | const count = 5 | ||
176 | const sort = 'createdAt' | ||
177 | |||
178 | { | ||
179 | const search = servers[0].port + '' | ||
180 | |||
181 | { | ||
182 | const body = await servers[2].follows.getFollowers({ start, count, sort, search }) | ||
183 | expect(body.total).to.equal(1) | ||
184 | |||
185 | const follows = body.data | ||
186 | expect(follows).to.have.lengthOf(1) | ||
187 | expect(follows[0].following.host).to.equal(servers[2].host) | ||
188 | } | ||
189 | |||
190 | { | ||
191 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' }) | ||
192 | expect(body.total).to.equal(1) | ||
193 | expect(body.data).to.have.lengthOf(1) | ||
194 | } | ||
195 | |||
196 | { | ||
197 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) | ||
198 | expect(body.total).to.equal(0) | ||
199 | expect(body.data).to.have.lengthOf(0) | ||
200 | } | ||
201 | |||
202 | { | ||
203 | const body = await servers[2].follows.getFollowers({ | ||
204 | start, | ||
205 | count, | ||
206 | sort, | ||
207 | search, | ||
208 | state: 'accepted', | ||
209 | actorType: 'Application' | ||
210 | }) | ||
211 | expect(body.total).to.equal(1) | ||
212 | expect(body.data).to.have.lengthOf(1) | ||
213 | } | ||
214 | |||
215 | { | ||
216 | const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' }) | ||
217 | expect(body.total).to.equal(0) | ||
218 | expect(body.data).to.have.lengthOf(0) | ||
219 | } | ||
220 | } | ||
221 | |||
222 | { | ||
223 | const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' }) | ||
224 | expect(body.total).to.equal(0) | ||
225 | |||
226 | const follows = body.data | ||
227 | expect(follows).to.have.lengthOf(0) | ||
228 | } | ||
229 | }) | ||
230 | |||
231 | it('Should have the correct follows counts', async function () { | ||
232 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) | ||
233 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | ||
234 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
235 | |||
236 | // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) | ||
237 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
238 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | ||
239 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
240 | |||
241 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
242 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
243 | }) | ||
244 | |||
245 | it('Should unfollow server 3 on server 1', async function () { | ||
246 | this.timeout(15000) | ||
247 | |||
248 | await servers[0].follows.unfollow({ target: servers[2] }) | ||
249 | |||
250 | await waitJobs(servers) | ||
251 | }) | ||
252 | |||
253 | it('Should not follow server 3 on server 1 anymore', async function () { | ||
254 | const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' }) | ||
255 | expect(body.total).to.equal(1) | ||
256 | |||
257 | const follows = body.data | ||
258 | expect(follows).to.be.an('array') | ||
259 | expect(follows).to.have.lengthOf(1) | ||
260 | |||
261 | expect(follows[0].following.host).to.equal(servers[1].host) | ||
262 | }) | ||
263 | |||
264 | it('Should not have server 1 as follower on server 3 anymore', async function () { | ||
265 | const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) | ||
266 | expect(body.total).to.equal(0) | ||
267 | |||
268 | const follows = body.data | ||
269 | expect(follows).to.be.an('array') | ||
270 | expect(follows).to.have.lengthOf(0) | ||
271 | }) | ||
272 | |||
273 | it('Should have the correct follows counts after the unfollow', async function () { | ||
274 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
275 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | ||
276 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) | ||
277 | |||
278 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
279 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) | ||
280 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
281 | |||
282 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 }) | ||
283 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) | ||
284 | }) | ||
285 | |||
286 | it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { | ||
287 | this.timeout(160000) | ||
288 | |||
289 | await servers[1].videos.upload({ attributes: { name: 'server2' } }) | ||
290 | await servers[2].videos.upload({ attributes: { name: 'server3' } }) | ||
291 | |||
292 | await waitJobs(servers) | ||
293 | |||
294 | { | ||
295 | const { total, data } = await servers[0].videos.list() | ||
296 | expect(total).to.equal(1) | ||
297 | expect(data[0].name).to.equal('server2') | ||
298 | } | ||
299 | |||
300 | { | ||
301 | const { total, data } = await servers[1].videos.list() | ||
302 | expect(total).to.equal(1) | ||
303 | expect(data[0].name).to.equal('server2') | ||
304 | } | ||
305 | |||
306 | { | ||
307 | const { total, data } = await servers[2].videos.list() | ||
308 | expect(total).to.equal(1) | ||
309 | expect(data[0].name).to.equal('server3') | ||
310 | } | ||
311 | }) | ||
312 | |||
313 | it('Should remove account follow', async function () { | ||
314 | this.timeout(15000) | ||
315 | |||
316 | await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) | ||
317 | |||
318 | await waitJobs(servers) | ||
319 | }) | ||
320 | |||
321 | it('Should have removed the account follow', async function () { | ||
322 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
323 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
324 | |||
325 | { | ||
326 | const { total, data } = await servers[0].follows.getFollowings() | ||
327 | expect(total).to.equal(0) | ||
328 | expect(data).to.have.lengthOf(0) | ||
329 | } | ||
330 | |||
331 | { | ||
332 | const { total, data } = await servers[0].videos.list() | ||
333 | expect(total).to.equal(0) | ||
334 | expect(data).to.have.lengthOf(0) | ||
335 | } | ||
336 | }) | ||
337 | |||
338 | it('Should follow a channel', async function () { | ||
339 | this.timeout(15000) | ||
340 | |||
341 | await servers[0].follows.follow({ | ||
342 | handles: [ 'root_channel@' + servers[1].host ] | ||
343 | }) | ||
344 | |||
345 | await waitJobs(servers) | ||
346 | |||
347 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
348 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
349 | |||
350 | { | ||
351 | const { total, data } = await servers[0].follows.getFollowings() | ||
352 | expect(total).to.equal(1) | ||
353 | expect(data).to.have.lengthOf(1) | ||
354 | } | ||
355 | |||
356 | { | ||
357 | const { total, data } = await servers[0].videos.list() | ||
358 | expect(total).to.equal(1) | ||
359 | expect(data).to.have.lengthOf(1) | ||
360 | } | ||
361 | }) | ||
362 | }) | ||
363 | |||
364 | describe('Should propagate data on a new server follow', function () { | ||
365 | let video4: Video | ||
366 | |||
367 | before(async function () { | ||
368 | this.timeout(240000) | ||
369 | |||
370 | const video4Attributes = { | ||
371 | name: 'server3-4', | ||
372 | category: 2, | ||
373 | nsfw: true, | ||
374 | licence: 6, | ||
375 | tags: [ 'tag1', 'tag2', 'tag3' ] | ||
376 | } | ||
377 | |||
378 | await servers[2].videos.upload({ attributes: { name: 'server3-2' } }) | ||
379 | await servers[2].videos.upload({ attributes: { name: 'server3-3' } }) | ||
380 | |||
381 | const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes }) | ||
382 | |||
383 | await servers[2].videos.upload({ attributes: { name: 'server3-5' } }) | ||
384 | await servers[2].videos.upload({ attributes: { name: 'server3-6' } }) | ||
385 | |||
386 | { | ||
387 | const userAccessToken = await servers[2].users.generateUserAndToken('captain') | ||
388 | |||
389 | await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' }) | ||
390 | await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' }) | ||
391 | } | ||
392 | |||
393 | { | ||
394 | await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' }) | ||
395 | |||
396 | await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' }) | ||
397 | await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' }) | ||
398 | await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' }) | ||
399 | } | ||
400 | |||
401 | { | ||
402 | const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' }) | ||
403 | await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' }) | ||
404 | |||
405 | const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' }) | ||
406 | |||
407 | await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' }) | ||
408 | |||
409 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId }) | ||
410 | await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId }) | ||
411 | } | ||
412 | |||
413 | await servers[2].captions.add({ | ||
414 | language: 'ar', | ||
415 | videoId: video4CreateResult.id, | ||
416 | fixture: 'subtitle-good2.vtt' | ||
417 | }) | ||
418 | |||
419 | await waitJobs(servers) | ||
420 | |||
421 | // Server 1 follows server 3 | ||
422 | await servers[0].follows.follow({ hosts: [ servers[2].url ] }) | ||
423 | |||
424 | await waitJobs(servers) | ||
425 | }) | ||
426 | |||
427 | it('Should have the correct follows counts', async function () { | ||
428 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) | ||
429 | await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
430 | await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
431 | await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
432 | |||
433 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
434 | await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) | ||
435 | await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) | ||
436 | await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) | ||
437 | |||
438 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) | ||
439 | await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) | ||
440 | }) | ||
441 | |||
442 | it('Should have propagated videos', async function () { | ||
443 | const { total, data } = await servers[0].videos.list() | ||
444 | expect(total).to.equal(7) | ||
445 | |||
446 | const video2 = data.find(v => v.name === 'server3-2') | ||
447 | video4 = data.find(v => v.name === 'server3-4') | ||
448 | const video6 = data.find(v => v.name === 'server3-6') | ||
449 | |||
450 | expect(video2).to.not.be.undefined | ||
451 | expect(video4).to.not.be.undefined | ||
452 | expect(video6).to.not.be.undefined | ||
453 | |||
454 | const isLocal = false | ||
455 | const checkAttributes = { | ||
456 | name: 'server3-4', | ||
457 | category: 2, | ||
458 | licence: 6, | ||
459 | language: 'zh', | ||
460 | nsfw: true, | ||
461 | description: 'my super description', | ||
462 | support: 'my super support text', | ||
463 | account: { | ||
464 | name: 'root', | ||
465 | host: servers[2].host | ||
466 | }, | ||
467 | isLocal, | ||
468 | commentsEnabled: true, | ||
469 | downloadEnabled: true, | ||
470 | duration: 5, | ||
471 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
472 | privacy: VideoPrivacy.PUBLIC, | ||
473 | likes: 1, | ||
474 | dislikes: 1, | ||
475 | channel: { | ||
476 | displayName: 'Main root channel', | ||
477 | name: 'root_channel', | ||
478 | description: '', | ||
479 | isLocal | ||
480 | }, | ||
481 | fixture: 'video_short.webm', | ||
482 | files: [ | ||
483 | { | ||
484 | resolution: 720, | ||
485 | size: 218910 | ||
486 | } | ||
487 | ] | ||
488 | } | ||
489 | await completeVideoCheck({ | ||
490 | server: servers[0], | ||
491 | originServer: servers[2], | ||
492 | videoUUID: video4.uuid, | ||
493 | attributes: checkAttributes | ||
494 | }) | ||
495 | }) | ||
496 | |||
497 | it('Should have propagated comments', async function () { | ||
498 | const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' }) | ||
499 | |||
500 | expect(total).to.equal(2) | ||
501 | expect(data).to.be.an('array') | ||
502 | expect(data).to.have.lengthOf(2) | ||
503 | |||
504 | { | ||
505 | const comment = data[0] | ||
506 | expect(comment.inReplyToCommentId).to.be.null | ||
507 | expect(comment.text).equal('my super first comment') | ||
508 | expect(comment.videoId).to.equal(video4.id) | ||
509 | expect(comment.id).to.equal(comment.threadId) | ||
510 | expect(comment.account.name).to.equal('root') | ||
511 | expect(comment.account.host).to.equal(servers[2].host) | ||
512 | expect(comment.totalReplies).to.equal(3) | ||
513 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
514 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
515 | |||
516 | const threadId = comment.threadId | ||
517 | |||
518 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId }) | ||
519 | expect(tree.comment.text).equal('my super first comment') | ||
520 | expect(tree.children).to.have.lengthOf(2) | ||
521 | |||
522 | const firstChild = tree.children[0] | ||
523 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
524 | expect(firstChild.children).to.have.lengthOf(1) | ||
525 | |||
526 | const childOfFirstChild = firstChild.children[0] | ||
527 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
528 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
529 | |||
530 | const secondChild = tree.children[1] | ||
531 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | ||
532 | expect(secondChild.children).to.have.lengthOf(0) | ||
533 | } | ||
534 | |||
535 | { | ||
536 | const deletedComment = data[1] | ||
537 | expect(deletedComment).to.not.be.undefined | ||
538 | expect(deletedComment.isDeleted).to.be.true | ||
539 | expect(deletedComment.deletedAt).to.not.be.null | ||
540 | expect(deletedComment.text).to.equal('') | ||
541 | expect(deletedComment.inReplyToCommentId).to.be.null | ||
542 | expect(deletedComment.account).to.be.null | ||
543 | expect(deletedComment.totalReplies).to.equal(2) | ||
544 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true | ||
545 | |||
546 | const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId }) | ||
547 | const [ commentRoot, deletedChildRoot ] = tree.children | ||
548 | |||
549 | expect(deletedChildRoot).to.not.be.undefined | ||
550 | expect(deletedChildRoot.comment.isDeleted).to.be.true | ||
551 | expect(deletedChildRoot.comment.deletedAt).to.not.be.null | ||
552 | expect(deletedChildRoot.comment.text).to.equal('') | ||
553 | expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
554 | expect(deletedChildRoot.comment.account).to.be.null | ||
555 | expect(deletedChildRoot.children).to.have.lengthOf(1) | ||
556 | |||
557 | const answerToDeletedChild = deletedChildRoot.children[0] | ||
558 | expect(answerToDeletedChild.comment).to.not.be.undefined | ||
559 | expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id) | ||
560 | expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted') | ||
561 | expect(answerToDeletedChild.comment.account.name).to.equal('root') | ||
562 | |||
563 | expect(commentRoot.comment).to.not.be.undefined | ||
564 | expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) | ||
565 | expect(commentRoot.comment.text).to.equal('answer to deleted') | ||
566 | expect(commentRoot.comment.account.name).to.equal('root') | ||
567 | } | ||
568 | }) | ||
569 | |||
570 | it('Should have propagated captions', async function () { | ||
571 | const body = await servers[0].captions.list({ videoId: video4.id }) | ||
572 | expect(body.total).to.equal(1) | ||
573 | expect(body.data).to.have.lengthOf(1) | ||
574 | |||
575 | const caption1 = body.data[0] | ||
576 | expect(caption1.language.id).to.equal('ar') | ||
577 | expect(caption1.language.label).to.equal('Arabic') | ||
578 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$')) | ||
579 | await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') | ||
580 | }) | ||
581 | |||
582 | it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () { | ||
583 | this.timeout(5000) | ||
584 | |||
585 | await servers[0].follows.unfollow({ target: servers[2] }) | ||
586 | |||
587 | await waitJobs(servers) | ||
588 | |||
589 | const { total } = await servers[0].videos.list() | ||
590 | expect(total).to.equal(1) | ||
591 | }) | ||
592 | }) | ||
593 | |||
594 | after(async function () { | ||
595 | await cleanupTests(servers) | ||
596 | }) | ||
597 | }) | ||
598 | |||
599 | describe('Simple data propagation propagate data on a new channel follow', function () { | ||
600 | let servers: PeerTubeServer[] = [] | ||
601 | |||
602 | before(async function () { | ||
603 | this.timeout(120000) | ||
604 | |||
605 | servers = await createMultipleServers(3) | ||
606 | await setAccessTokensToServers(servers) | ||
607 | |||
608 | await servers[0].videos.upload({ attributes: { name: 'video to add' } }) | ||
609 | |||
610 | await waitJobs(servers) | ||
611 | |||
612 | for (const server of [ servers[1], servers[2] ]) { | ||
613 | const video = await server.videos.find({ name: 'video to add' }) | ||
614 | expect(video).to.not.exist | ||
615 | } | ||
616 | }) | ||
617 | |||
618 | it('Should have propagated video after new channel follow', async function () { | ||
619 | this.timeout(60000) | ||
620 | |||
621 | await servers[1].follows.follow({ handles: [ 'root_channel@' + servers[0].host ] }) | ||
622 | |||
623 | await waitJobs(servers) | ||
624 | |||
625 | const video = await servers[1].videos.find({ name: 'video to add' }) | ||
626 | expect(video).to.exist | ||
627 | }) | ||
628 | |||
629 | it('Should have propagated video after new account follow', async function () { | ||
630 | this.timeout(60000) | ||
631 | |||
632 | await servers[2].follows.follow({ handles: [ 'root@' + servers[0].host ] }) | ||
633 | |||
634 | await waitJobs(servers) | ||
635 | |||
636 | const video = await servers[2].videos.find({ name: 'video to add' }) | ||
637 | expect(video).to.exist | ||
638 | }) | ||
639 | |||
640 | after(async function () { | ||
641 | await cleanupTests(servers) | ||
642 | }) | ||
643 | }) | ||
644 | }) | ||
diff --git a/packages/tests/src/api/server/handle-down.ts b/packages/tests/src/api/server/handle-down.ts new file mode 100644 index 000000000..604df129f --- /dev/null +++ b/packages/tests/src/api/server/handle-down.ts | |||
@@ -0,0 +1,339 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, JobState, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | CommentsCommand, | ||
9 | createMultipleServers, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
16 | import { completeVideoCheck } from '@tests/shared/videos.js' | ||
17 | |||
18 | describe('Test handle downs', function () { | ||
19 | let servers: PeerTubeServer[] = [] | ||
20 | let sqlCommands: SQLCommand[] = [] | ||
21 | |||
22 | let threadIdServer1: number | ||
23 | let threadIdServer2: number | ||
24 | let commentIdServer1: number | ||
25 | let commentIdServer2: number | ||
26 | let missedVideo1: VideoCreateResult | ||
27 | let missedVideo2: VideoCreateResult | ||
28 | let unlistedVideo: VideoCreateResult | ||
29 | |||
30 | const videoIdsServer1: string[] = [] | ||
31 | |||
32 | const videoAttributes = { | ||
33 | name: 'my super name for server 1', | ||
34 | category: 5, | ||
35 | licence: 4, | ||
36 | language: 'ja', | ||
37 | nsfw: true, | ||
38 | privacy: VideoPrivacy.PUBLIC, | ||
39 | description: 'my super description for server 1', | ||
40 | support: 'my super support text for server 1', | ||
41 | tags: [ 'tag1p1', 'tag2p1' ], | ||
42 | fixture: 'video_short1.webm' | ||
43 | } | ||
44 | |||
45 | const unlistedVideoAttributes = { ...videoAttributes, privacy: VideoPrivacy.UNLISTED } | ||
46 | |||
47 | let checkAttributes: any | ||
48 | let unlistedCheckAttributes: any | ||
49 | |||
50 | let commentCommands: CommentsCommand[] | ||
51 | |||
52 | before(async function () { | ||
53 | this.timeout(120000) | ||
54 | |||
55 | servers = await createMultipleServers(3) | ||
56 | commentCommands = servers.map(s => s.comments) | ||
57 | |||
58 | checkAttributes = { | ||
59 | name: 'my super name for server 1', | ||
60 | category: 5, | ||
61 | licence: 4, | ||
62 | language: 'ja', | ||
63 | nsfw: true, | ||
64 | description: 'my super description for server 1', | ||
65 | support: 'my super support text for server 1', | ||
66 | account: { | ||
67 | name: 'root', | ||
68 | host: servers[0].host | ||
69 | }, | ||
70 | isLocal: false, | ||
71 | duration: 10, | ||
72 | tags: [ 'tag1p1', 'tag2p1' ], | ||
73 | privacy: VideoPrivacy.PUBLIC, | ||
74 | commentsEnabled: true, | ||
75 | downloadEnabled: true, | ||
76 | channel: { | ||
77 | name: 'root_channel', | ||
78 | displayName: 'Main root channel', | ||
79 | description: '', | ||
80 | isLocal: false | ||
81 | }, | ||
82 | fixture: 'video_short1.webm', | ||
83 | files: [ | ||
84 | { | ||
85 | resolution: 720, | ||
86 | size: 572456 | ||
87 | } | ||
88 | ] | ||
89 | } | ||
90 | unlistedCheckAttributes = { ...checkAttributes, privacy: VideoPrivacy.UNLISTED } | ||
91 | |||
92 | // Get the access tokens | ||
93 | await setAccessTokensToServers(servers) | ||
94 | |||
95 | sqlCommands = servers.map(s => new SQLCommand(s)) | ||
96 | }) | ||
97 | |||
98 | it('Should remove followers that are often down', async function () { | ||
99 | this.timeout(240000) | ||
100 | |||
101 | // Server 2 and 3 follow server 1 | ||
102 | await servers[1].follows.follow({ hosts: [ servers[0].url ] }) | ||
103 | await servers[2].follows.follow({ hosts: [ servers[0].url ] }) | ||
104 | |||
105 | await waitJobs(servers) | ||
106 | |||
107 | // Upload a video to server 1 | ||
108 | await servers[0].videos.upload({ attributes: videoAttributes }) | ||
109 | |||
110 | await waitJobs(servers) | ||
111 | |||
112 | // And check all servers have this video | ||
113 | for (const server of servers) { | ||
114 | const { data } = await server.videos.list() | ||
115 | expect(data).to.be.an('array') | ||
116 | expect(data).to.have.lengthOf(1) | ||
117 | } | ||
118 | |||
119 | // Kill server 2 | ||
120 | await killallServers([ servers[1] ]) | ||
121 | |||
122 | // Remove server 2 follower | ||
123 | for (let i = 0; i < 10; i++) { | ||
124 | await servers[0].videos.upload({ attributes: videoAttributes }) | ||
125 | } | ||
126 | |||
127 | await waitJobs([ servers[0], servers[2] ]) | ||
128 | |||
129 | // Kill server 3 | ||
130 | await killallServers([ servers[2] ]) | ||
131 | |||
132 | missedVideo1 = await servers[0].videos.upload({ attributes: videoAttributes }) | ||
133 | |||
134 | missedVideo2 = await servers[0].videos.upload({ attributes: videoAttributes }) | ||
135 | |||
136 | // Unlisted video | ||
137 | unlistedVideo = await servers[0].videos.upload({ attributes: unlistedVideoAttributes }) | ||
138 | |||
139 | // Add comments to video 2 | ||
140 | { | ||
141 | const text = 'thread 1' | ||
142 | let comment = await commentCommands[0].createThread({ videoId: missedVideo2.uuid, text }) | ||
143 | threadIdServer1 = comment.id | ||
144 | |||
145 | comment = await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: comment.id, text: 'comment 1-1' }) | ||
146 | |||
147 | const created = await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: comment.id, text: 'comment 1-2' }) | ||
148 | commentIdServer1 = created.id | ||
149 | } | ||
150 | |||
151 | await waitJobs(servers[0]) | ||
152 | // Wait scheduler | ||
153 | await wait(11000) | ||
154 | |||
155 | // Only server 3 is still a follower of server 1 | ||
156 | const body = await servers[0].follows.getFollowers({ start: 0, count: 2, sort: 'createdAt' }) | ||
157 | expect(body.data).to.be.an('array') | ||
158 | expect(body.data).to.have.lengthOf(1) | ||
159 | expect(body.data[0].follower.host).to.equal(servers[2].host) | ||
160 | }) | ||
161 | |||
162 | it('Should not have pending/processing jobs anymore', async function () { | ||
163 | const states: JobState[] = [ 'waiting', 'active' ] | ||
164 | |||
165 | for (const state of states) { | ||
166 | const body = await servers[0].jobs.list({ | ||
167 | state, | ||
168 | start: 0, | ||
169 | count: 50, | ||
170 | sort: '-createdAt' | ||
171 | }) | ||
172 | expect(body.data).to.have.length(0) | ||
173 | } | ||
174 | }) | ||
175 | |||
176 | it('Should re-follow server 1', async function () { | ||
177 | this.timeout(70000) | ||
178 | |||
179 | await servers[1].run() | ||
180 | await servers[2].run() | ||
181 | |||
182 | await servers[1].follows.unfollow({ target: servers[0] }) | ||
183 | await waitJobs(servers) | ||
184 | |||
185 | await servers[1].follows.follow({ hosts: [ servers[0].url ] }) | ||
186 | |||
187 | await waitJobs(servers) | ||
188 | |||
189 | const body = await servers[0].follows.getFollowers({ start: 0, count: 2, sort: 'createdAt' }) | ||
190 | expect(body.data).to.be.an('array') | ||
191 | expect(body.data).to.have.lengthOf(2) | ||
192 | }) | ||
193 | |||
194 | it('Should send an update to server 3, and automatically fetch the video', async function () { | ||
195 | this.timeout(15000) | ||
196 | |||
197 | { | ||
198 | const { data } = await servers[2].videos.list() | ||
199 | expect(data).to.be.an('array') | ||
200 | expect(data).to.have.lengthOf(11) | ||
201 | } | ||
202 | |||
203 | await servers[0].videos.update({ id: missedVideo1.uuid }) | ||
204 | await servers[0].videos.update({ id: unlistedVideo.uuid }) | ||
205 | |||
206 | await waitJobs(servers) | ||
207 | |||
208 | { | ||
209 | const { data } = await servers[2].videos.list() | ||
210 | expect(data).to.be.an('array') | ||
211 | // 1 video is unlisted | ||
212 | expect(data).to.have.lengthOf(12) | ||
213 | } | ||
214 | |||
215 | // Check unlisted video | ||
216 | const video = await servers[2].videos.get({ id: unlistedVideo.uuid }) | ||
217 | await completeVideoCheck({ server: servers[2], originServer: servers[0], videoUUID: video.uuid, attributes: unlistedCheckAttributes }) | ||
218 | }) | ||
219 | |||
220 | it('Should send comments on a video to server 3, and automatically fetch the video', async function () { | ||
221 | this.timeout(25000) | ||
222 | |||
223 | await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: commentIdServer1, text: 'comment 1-3' }) | ||
224 | |||
225 | await waitJobs(servers) | ||
226 | |||
227 | await servers[2].videos.get({ id: missedVideo2.uuid }) | ||
228 | |||
229 | { | ||
230 | const { data } = await servers[2].comments.listThreads({ videoId: missedVideo2.uuid }) | ||
231 | expect(data).to.be.an('array') | ||
232 | expect(data).to.have.lengthOf(1) | ||
233 | |||
234 | threadIdServer2 = data[0].id | ||
235 | |||
236 | const tree = await servers[2].comments.getThread({ videoId: missedVideo2.uuid, threadId: threadIdServer2 }) | ||
237 | expect(tree.comment.text).equal('thread 1') | ||
238 | expect(tree.children).to.have.lengthOf(1) | ||
239 | |||
240 | const firstChild = tree.children[0] | ||
241 | expect(firstChild.comment.text).to.equal('comment 1-1') | ||
242 | expect(firstChild.children).to.have.lengthOf(1) | ||
243 | |||
244 | const childOfFirstChild = firstChild.children[0] | ||
245 | expect(childOfFirstChild.comment.text).to.equal('comment 1-2') | ||
246 | expect(childOfFirstChild.children).to.have.lengthOf(1) | ||
247 | |||
248 | const childOfChildFirstChild = childOfFirstChild.children[0] | ||
249 | expect(childOfChildFirstChild.comment.text).to.equal('comment 1-3') | ||
250 | expect(childOfChildFirstChild.children).to.have.lengthOf(0) | ||
251 | |||
252 | commentIdServer2 = childOfChildFirstChild.comment.id | ||
253 | } | ||
254 | }) | ||
255 | |||
256 | it('Should correctly reply to the comment', async function () { | ||
257 | this.timeout(15000) | ||
258 | |||
259 | await servers[2].comments.addReply({ videoId: missedVideo2.uuid, toCommentId: commentIdServer2, text: 'comment 1-4' }) | ||
260 | |||
261 | await waitJobs(servers) | ||
262 | |||
263 | const tree = await commentCommands[0].getThread({ videoId: missedVideo2.uuid, threadId: threadIdServer1 }) | ||
264 | |||
265 | expect(tree.comment.text).equal('thread 1') | ||
266 | expect(tree.children).to.have.lengthOf(1) | ||
267 | |||
268 | const firstChild = tree.children[0] | ||
269 | expect(firstChild.comment.text).to.equal('comment 1-1') | ||
270 | expect(firstChild.children).to.have.lengthOf(1) | ||
271 | |||
272 | const childOfFirstChild = firstChild.children[0] | ||
273 | expect(childOfFirstChild.comment.text).to.equal('comment 1-2') | ||
274 | expect(childOfFirstChild.children).to.have.lengthOf(1) | ||
275 | |||
276 | const childOfChildFirstChild = childOfFirstChild.children[0] | ||
277 | expect(childOfChildFirstChild.comment.text).to.equal('comment 1-3') | ||
278 | expect(childOfChildFirstChild.children).to.have.lengthOf(1) | ||
279 | |||
280 | const childOfChildOfChildOfFirstChild = childOfChildFirstChild.children[0] | ||
281 | expect(childOfChildOfChildOfFirstChild.comment.text).to.equal('comment 1-4') | ||
282 | expect(childOfChildOfChildOfFirstChild.children).to.have.lengthOf(0) | ||
283 | }) | ||
284 | |||
285 | it('Should upload many videos on server 1', async function () { | ||
286 | this.timeout(240000) | ||
287 | |||
288 | for (let i = 0; i < 10; i++) { | ||
289 | const uuid = (await servers[0].videos.quickUpload({ name: 'video ' + i })).uuid | ||
290 | videoIdsServer1.push(uuid) | ||
291 | } | ||
292 | |||
293 | await waitJobs(servers) | ||
294 | |||
295 | for (const id of videoIdsServer1) { | ||
296 | await servers[1].videos.get({ id }) | ||
297 | } | ||
298 | |||
299 | await waitJobs(servers) | ||
300 | await sqlCommands[1].setActorFollowScores(20) | ||
301 | |||
302 | // Wait video expiration | ||
303 | await wait(11000) | ||
304 | |||
305 | // Refresh video -> score + 10 = 30 | ||
306 | await servers[1].videos.get({ id: videoIdsServer1[0] }) | ||
307 | |||
308 | await waitJobs(servers) | ||
309 | }) | ||
310 | |||
311 | it('Should remove followings that are down', async function () { | ||
312 | this.timeout(120000) | ||
313 | |||
314 | await killallServers([ servers[0] ]) | ||
315 | |||
316 | // Wait video expiration | ||
317 | await wait(11000) | ||
318 | |||
319 | for (let i = 0; i < 5; i++) { | ||
320 | try { | ||
321 | await servers[1].videos.get({ id: videoIdsServer1[i] }) | ||
322 | await waitJobs([ servers[1] ]) | ||
323 | await wait(1500) | ||
324 | } catch {} | ||
325 | } | ||
326 | |||
327 | for (const id of videoIdsServer1) { | ||
328 | await servers[1].videos.get({ id, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
329 | } | ||
330 | }) | ||
331 | |||
332 | after(async function () { | ||
333 | for (const sqlCommand of sqlCommands) { | ||
334 | await sqlCommand.cleanup() | ||
335 | } | ||
336 | |||
337 | await cleanupTests(servers) | ||
338 | }) | ||
339 | }) | ||
diff --git a/packages/tests/src/api/server/homepage.ts b/packages/tests/src/api/server/homepage.ts new file mode 100644 index 000000000..082a2fb91 --- /dev/null +++ b/packages/tests/src/api/server/homepage.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | CustomPagesCommand, | ||
9 | killallServers, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | async function getHomepageState (server: PeerTubeServer) { | ||
17 | const config = await server.config.getConfig() | ||
18 | |||
19 | return config.homepage.enabled | ||
20 | } | ||
21 | |||
22 | describe('Test instance homepage actions', function () { | ||
23 | let server: PeerTubeServer | ||
24 | let command: CustomPagesCommand | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(30000) | ||
28 | |||
29 | server = await createSingleServer(1) | ||
30 | await setAccessTokensToServers([ server ]) | ||
31 | await setDefaultChannelAvatar(server) | ||
32 | await setDefaultAccountAvatar(server) | ||
33 | |||
34 | command = server.customPage | ||
35 | }) | ||
36 | |||
37 | it('Should not have a homepage', async function () { | ||
38 | const state = await getHomepageState(server) | ||
39 | expect(state).to.be.false | ||
40 | |||
41 | await command.getInstanceHomepage({ expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
42 | }) | ||
43 | |||
44 | it('Should set a homepage', async function () { | ||
45 | await command.updateInstanceHomepage({ content: '<picsou-magazine></picsou-magazine>' }) | ||
46 | |||
47 | const page = await command.getInstanceHomepage() | ||
48 | expect(page.content).to.equal('<picsou-magazine></picsou-magazine>') | ||
49 | |||
50 | const state = await getHomepageState(server) | ||
51 | expect(state).to.be.true | ||
52 | }) | ||
53 | |||
54 | it('Should have the same homepage after a restart', async function () { | ||
55 | this.timeout(30000) | ||
56 | |||
57 | await killallServers([ server ]) | ||
58 | |||
59 | await server.run() | ||
60 | |||
61 | const page = await command.getInstanceHomepage() | ||
62 | expect(page.content).to.equal('<picsou-magazine></picsou-magazine>') | ||
63 | |||
64 | const state = await getHomepageState(server) | ||
65 | expect(state).to.be.true | ||
66 | }) | ||
67 | |||
68 | it('Should empty the homepage', async function () { | ||
69 | await command.updateInstanceHomepage({ content: '' }) | ||
70 | |||
71 | const page = await command.getInstanceHomepage() | ||
72 | expect(page.content).to.be.empty | ||
73 | |||
74 | const state = await getHomepageState(server) | ||
75 | expect(state).to.be.false | ||
76 | }) | ||
77 | |||
78 | after(async function () { | ||
79 | await cleanupTests([ server ]) | ||
80 | }) | ||
81 | }) | ||
diff --git a/packages/tests/src/api/server/index.ts b/packages/tests/src/api/server/index.ts new file mode 100644 index 000000000..5c80a5a37 --- /dev/null +++ b/packages/tests/src/api/server/index.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import './auto-follows.js' | ||
2 | import './bulk.js' | ||
3 | import './config-defaults.js' | ||
4 | import './config.js' | ||
5 | import './contact-form.js' | ||
6 | import './email.js' | ||
7 | import './follow-constraints.js' | ||
8 | import './follows.js' | ||
9 | import './follows-moderation.js' | ||
10 | import './homepage.js' | ||
11 | import './handle-down.js' | ||
12 | import './jobs.js' | ||
13 | import './logs.js' | ||
14 | import './reverse-proxy.js' | ||
15 | import './services.js' | ||
16 | import './slow-follows.js' | ||
17 | import './stats.js' | ||
18 | import './tracker.js' | ||
19 | import './no-client.js' | ||
20 | import './open-telemetry.js' | ||
21 | import './plugins.js' | ||
22 | import './proxy.js' | ||
diff --git a/packages/tests/src/api/server/jobs.ts b/packages/tests/src/api/server/jobs.ts new file mode 100644 index 000000000..3d60b1431 --- /dev/null +++ b/packages/tests/src/api/server/jobs.ts | |||
@@ -0,0 +1,128 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { dateIsValid } from '@tests/shared/checks.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test jobs', function () { | ||
16 | let servers: PeerTubeServer[] | ||
17 | |||
18 | before(async function () { | ||
19 | this.timeout(240000) | ||
20 | |||
21 | servers = await createMultipleServers(2) | ||
22 | |||
23 | await setAccessTokensToServers(servers) | ||
24 | |||
25 | // Server 1 and server 2 follow each other | ||
26 | await doubleFollow(servers[0], servers[1]) | ||
27 | }) | ||
28 | |||
29 | it('Should create some jobs', async function () { | ||
30 | this.timeout(240000) | ||
31 | |||
32 | await servers[1].videos.upload({ attributes: { name: 'video1' } }) | ||
33 | await servers[1].videos.upload({ attributes: { name: 'video2' } }) | ||
34 | |||
35 | await waitJobs(servers) | ||
36 | }) | ||
37 | |||
38 | it('Should list jobs', async function () { | ||
39 | const body = await servers[1].jobs.list({ state: 'completed' }) | ||
40 | expect(body.total).to.be.above(2) | ||
41 | expect(body.data).to.have.length.above(2) | ||
42 | }) | ||
43 | |||
44 | it('Should list jobs with sort, pagination and job type', async function () { | ||
45 | { | ||
46 | const body = await servers[1].jobs.list({ | ||
47 | state: 'completed', | ||
48 | start: 1, | ||
49 | count: 2, | ||
50 | sort: 'createdAt' | ||
51 | }) | ||
52 | expect(body.total).to.be.above(2) | ||
53 | expect(body.data).to.have.lengthOf(2) | ||
54 | |||
55 | let job = body.data[0] | ||
56 | // Skip repeat jobs | ||
57 | if (job.type === 'videos-views-stats') job = body.data[1] | ||
58 | |||
59 | expect(job.state).to.equal('completed') | ||
60 | expect(dateIsValid(job.createdAt as string)).to.be.true | ||
61 | expect(dateIsValid(job.processedOn as string)).to.be.true | ||
62 | expect(dateIsValid(job.finishedOn as string)).to.be.true | ||
63 | } | ||
64 | |||
65 | { | ||
66 | const body = await servers[1].jobs.list({ | ||
67 | state: 'completed', | ||
68 | start: 0, | ||
69 | count: 100, | ||
70 | sort: 'createdAt', | ||
71 | jobType: 'activitypub-http-broadcast' | ||
72 | }) | ||
73 | expect(body.total).to.be.above(2) | ||
74 | |||
75 | for (const j of body.data) { | ||
76 | expect(j.type).to.equal('activitypub-http-broadcast') | ||
77 | } | ||
78 | } | ||
79 | }) | ||
80 | |||
81 | it('Should list all jobs', async function () { | ||
82 | const body = await servers[1].jobs.list() | ||
83 | expect(body.total).to.be.above(2) | ||
84 | |||
85 | const jobs = body.data | ||
86 | expect(jobs).to.have.length.above(2) | ||
87 | |||
88 | expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined | ||
89 | }) | ||
90 | |||
91 | it('Should pause the job queue', async function () { | ||
92 | this.timeout(120000) | ||
93 | |||
94 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video2' } }) | ||
95 | await waitJobs(servers) | ||
96 | |||
97 | await servers[1].jobs.pauseJobQueue() | ||
98 | await servers[1].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' }) | ||
99 | |||
100 | await wait(5000) | ||
101 | |||
102 | { | ||
103 | const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' }) | ||
104 | // waiting includes waiting-children | ||
105 | expect(body.data).to.have.lengthOf(4) | ||
106 | } | ||
107 | |||
108 | { | ||
109 | const body = await servers[1].jobs.list({ state: 'waiting-children', jobType: 'video-transcoding' }) | ||
110 | expect(body.data).to.have.lengthOf(1) | ||
111 | } | ||
112 | }) | ||
113 | |||
114 | it('Should resume the job queue', async function () { | ||
115 | this.timeout(120000) | ||
116 | |||
117 | await servers[1].jobs.resumeJobQueue() | ||
118 | |||
119 | await waitJobs(servers) | ||
120 | |||
121 | const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' }) | ||
122 | expect(body.data).to.have.lengthOf(0) | ||
123 | }) | ||
124 | |||
125 | after(async function () { | ||
126 | await cleanupTests(servers) | ||
127 | }) | ||
128 | }) | ||
diff --git a/packages/tests/src/api/server/logs.ts b/packages/tests/src/api/server/logs.ts new file mode 100644 index 000000000..11c86d694 --- /dev/null +++ b/packages/tests/src/api/server/logs.ts | |||
@@ -0,0 +1,265 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | killallServers, | ||
9 | LogsCommand, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test logs', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let logsCommand: LogsCommand | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(30000) | ||
21 | |||
22 | server = await createSingleServer(1) | ||
23 | await setAccessTokensToServers([ server ]) | ||
24 | |||
25 | logsCommand = server.logs | ||
26 | }) | ||
27 | |||
28 | describe('With the standard log file', function () { | ||
29 | |||
30 | it('Should get logs with a start date', async function () { | ||
31 | this.timeout(60000) | ||
32 | |||
33 | await server.videos.upload({ attributes: { name: 'video 1' } }) | ||
34 | await waitJobs([ server ]) | ||
35 | |||
36 | const now = new Date() | ||
37 | |||
38 | await server.videos.upload({ attributes: { name: 'video 2' } }) | ||
39 | await waitJobs([ server ]) | ||
40 | |||
41 | const body = await logsCommand.getLogs({ startDate: now }) | ||
42 | const logsString = JSON.stringify(body) | ||
43 | |||
44 | expect(logsString.includes('Video with name video 1')).to.be.false | ||
45 | expect(logsString.includes('Video with name video 2')).to.be.true | ||
46 | }) | ||
47 | |||
48 | it('Should get logs with an end date', async function () { | ||
49 | this.timeout(60000) | ||
50 | |||
51 | await server.videos.upload({ attributes: { name: 'video 3' } }) | ||
52 | await waitJobs([ server ]) | ||
53 | |||
54 | const now1 = new Date() | ||
55 | |||
56 | await server.videos.upload({ attributes: { name: 'video 4' } }) | ||
57 | await waitJobs([ server ]) | ||
58 | |||
59 | const now2 = new Date() | ||
60 | |||
61 | await server.videos.upload({ attributes: { name: 'video 5' } }) | ||
62 | await waitJobs([ server ]) | ||
63 | |||
64 | const body = await logsCommand.getLogs({ startDate: now1, endDate: now2 }) | ||
65 | const logsString = JSON.stringify(body) | ||
66 | |||
67 | expect(logsString.includes('Video with name video 3')).to.be.false | ||
68 | expect(logsString.includes('Video with name video 4')).to.be.true | ||
69 | expect(logsString.includes('Video with name video 5')).to.be.false | ||
70 | }) | ||
71 | |||
72 | it('Should filter by level', async function () { | ||
73 | this.timeout(60000) | ||
74 | |||
75 | const now = new Date() | ||
76 | |||
77 | await server.videos.upload({ attributes: { name: 'video 6' } }) | ||
78 | await waitJobs([ server ]) | ||
79 | |||
80 | { | ||
81 | const body = await logsCommand.getLogs({ startDate: now, level: 'info' }) | ||
82 | const logsString = JSON.stringify(body) | ||
83 | |||
84 | expect(logsString.includes('Video with name video 6')).to.be.true | ||
85 | } | ||
86 | |||
87 | { | ||
88 | const body = await logsCommand.getLogs({ startDate: now, level: 'warn' }) | ||
89 | const logsString = JSON.stringify(body) | ||
90 | |||
91 | expect(logsString.includes('Video with name video 6')).to.be.false | ||
92 | } | ||
93 | }) | ||
94 | |||
95 | it('Should filter by tag', async function () { | ||
96 | const now = new Date() | ||
97 | |||
98 | const { uuid } = await server.videos.upload({ attributes: { name: 'video 6' } }) | ||
99 | await waitJobs([ server ]) | ||
100 | |||
101 | { | ||
102 | const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ 'toto' ] }) | ||
103 | expect(body).to.have.lengthOf(0) | ||
104 | } | ||
105 | |||
106 | { | ||
107 | const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ uuid ] }) | ||
108 | expect(body).to.not.have.lengthOf(0) | ||
109 | |||
110 | for (const line of body) { | ||
111 | expect(line.tags).to.contain(uuid) | ||
112 | } | ||
113 | } | ||
114 | }) | ||
115 | |||
116 | it('Should log ping requests', async function () { | ||
117 | const now = new Date() | ||
118 | |||
119 | await server.servers.ping() | ||
120 | |||
121 | const body = await logsCommand.getLogs({ startDate: now, level: 'info' }) | ||
122 | const logsString = JSON.stringify(body) | ||
123 | |||
124 | expect(logsString.includes('/api/v1/ping')).to.be.true | ||
125 | }) | ||
126 | |||
127 | it('Should not log ping requests', async function () { | ||
128 | this.timeout(60000) | ||
129 | |||
130 | await killallServers([ server ]) | ||
131 | |||
132 | await server.run({ log: { log_ping_requests: false } }) | ||
133 | |||
134 | const now = new Date() | ||
135 | |||
136 | await server.servers.ping() | ||
137 | |||
138 | const body = await logsCommand.getLogs({ startDate: now, level: 'info' }) | ||
139 | const logsString = JSON.stringify(body) | ||
140 | |||
141 | expect(logsString.includes('/api/v1/ping')).to.be.false | ||
142 | }) | ||
143 | }) | ||
144 | |||
145 | describe('With the audit log', function () { | ||
146 | |||
147 | it('Should get logs with a start date', async function () { | ||
148 | this.timeout(60000) | ||
149 | |||
150 | await server.videos.upload({ attributes: { name: 'video 7' } }) | ||
151 | await waitJobs([ server ]) | ||
152 | |||
153 | const now = new Date() | ||
154 | |||
155 | await server.videos.upload({ attributes: { name: 'video 8' } }) | ||
156 | await waitJobs([ server ]) | ||
157 | |||
158 | const body = await logsCommand.getAuditLogs({ startDate: now }) | ||
159 | const logsString = JSON.stringify(body) | ||
160 | |||
161 | expect(logsString.includes('video 7')).to.be.false | ||
162 | expect(logsString.includes('video 8')).to.be.true | ||
163 | |||
164 | expect(body).to.have.lengthOf(1) | ||
165 | |||
166 | const item = body[0] | ||
167 | |||
168 | const message = JSON.parse(item.message) | ||
169 | expect(message.domain).to.equal('videos') | ||
170 | expect(message.action).to.equal('create') | ||
171 | }) | ||
172 | |||
173 | it('Should get logs with an end date', async function () { | ||
174 | this.timeout(60000) | ||
175 | |||
176 | await server.videos.upload({ attributes: { name: 'video 9' } }) | ||
177 | await waitJobs([ server ]) | ||
178 | |||
179 | const now1 = new Date() | ||
180 | |||
181 | await server.videos.upload({ attributes: { name: 'video 10' } }) | ||
182 | await waitJobs([ server ]) | ||
183 | |||
184 | const now2 = new Date() | ||
185 | |||
186 | await server.videos.upload({ attributes: { name: 'video 11' } }) | ||
187 | await waitJobs([ server ]) | ||
188 | |||
189 | const body = await logsCommand.getAuditLogs({ startDate: now1, endDate: now2 }) | ||
190 | const logsString = JSON.stringify(body) | ||
191 | |||
192 | expect(logsString.includes('video 9')).to.be.false | ||
193 | expect(logsString.includes('video 10')).to.be.true | ||
194 | expect(logsString.includes('video 11')).to.be.false | ||
195 | }) | ||
196 | }) | ||
197 | |||
198 | describe('When creating log from the client', function () { | ||
199 | |||
200 | it('Should create a warn client log', async function () { | ||
201 | const now = new Date() | ||
202 | |||
203 | await server.logs.createLogClient({ | ||
204 | payload: { | ||
205 | level: 'warn', | ||
206 | url: 'http://example.com', | ||
207 | message: 'my super client message' | ||
208 | }, | ||
209 | token: null | ||
210 | }) | ||
211 | |||
212 | const body = await logsCommand.getLogs({ startDate: now }) | ||
213 | const logsString = JSON.stringify(body) | ||
214 | |||
215 | expect(logsString.includes('my super client message')).to.be.true | ||
216 | }) | ||
217 | |||
218 | it('Should create an error authenticated client log', async function () { | ||
219 | const now = new Date() | ||
220 | |||
221 | await server.logs.createLogClient({ | ||
222 | payload: { | ||
223 | url: 'https://example.com/page1', | ||
224 | level: 'error', | ||
225 | message: 'my super client message 2', | ||
226 | userAgent: 'super user agent', | ||
227 | meta: '{hello}', | ||
228 | stackTrace: 'super stack trace' | ||
229 | } | ||
230 | }) | ||
231 | |||
232 | const body = await logsCommand.getLogs({ startDate: now }) | ||
233 | const logsString = JSON.stringify(body) | ||
234 | |||
235 | expect(logsString.includes('my super client message 2')).to.be.true | ||
236 | expect(logsString.includes('super user agent')).to.be.true | ||
237 | expect(logsString.includes('super stack trace')).to.be.true | ||
238 | expect(logsString.includes('{hello}')).to.be.true | ||
239 | expect(logsString.includes('https://example.com/page1')).to.be.true | ||
240 | }) | ||
241 | |||
242 | it('Should refuse to create client logs', async function () { | ||
243 | await server.kill() | ||
244 | |||
245 | await server.run({ | ||
246 | log: { | ||
247 | accept_client_log: false | ||
248 | } | ||
249 | }) | ||
250 | |||
251 | await server.logs.createLogClient({ | ||
252 | payload: { | ||
253 | level: 'warn', | ||
254 | url: 'http://example.com', | ||
255 | message: 'my super client message' | ||
256 | }, | ||
257 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
258 | }) | ||
259 | }) | ||
260 | }) | ||
261 | |||
262 | after(async function () { | ||
263 | await cleanupTests([ server ]) | ||
264 | }) | ||
265 | }) | ||
diff --git a/packages/tests/src/api/server/no-client.ts b/packages/tests/src/api/server/no-client.ts new file mode 100644 index 000000000..0f097d50b --- /dev/null +++ b/packages/tests/src/api/server/no-client.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import request from 'supertest' | ||
2 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
3 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
4 | |||
5 | describe('Start and stop server without web client routes', function () { | ||
6 | let server: PeerTubeServer | ||
7 | |||
8 | before(async function () { | ||
9 | this.timeout(30000) | ||
10 | |||
11 | server = await createSingleServer(1, {}, { peertubeArgs: [ '--no-client' ] }) | ||
12 | }) | ||
13 | |||
14 | it('Should fail getting the client', function () { | ||
15 | const req = request(server.url) | ||
16 | .get('/') | ||
17 | |||
18 | return req.expect(HttpStatusCode.NOT_FOUND_404) | ||
19 | }) | ||
20 | |||
21 | after(async function () { | ||
22 | await cleanupTests([ server ]) | ||
23 | }) | ||
24 | }) | ||
diff --git a/packages/tests/src/api/server/open-telemetry.ts b/packages/tests/src/api/server/open-telemetry.ts new file mode 100644 index 000000000..8ed3801db --- /dev/null +++ b/packages/tests/src/api/server/open-telemetry.ts | |||
@@ -0,0 +1,193 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode, PlaybackMetricCreate, VideoPrivacy, VideoResolution } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeRawRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | import { expectLogDoesNotContain, expectLogContain } from '@tests/shared/checks.js' | ||
13 | import { MockHTTP } from '@tests/shared/mock-servers/mock-http.js' | ||
14 | |||
15 | describe('Open Telemetry', function () { | ||
16 | let server: PeerTubeServer | ||
17 | |||
18 | describe('Metrics', function () { | ||
19 | const metricsUrl = 'http://127.0.0.1:9092/metrics' | ||
20 | |||
21 | it('Should not enable open telemetry metrics', async function () { | ||
22 | this.timeout(60000) | ||
23 | |||
24 | server = await createSingleServer(1) | ||
25 | |||
26 | let hasError = false | ||
27 | try { | ||
28 | await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
29 | } catch (err) { | ||
30 | hasError = err.message.includes('ECONNREFUSED') | ||
31 | } | ||
32 | |||
33 | expect(hasError).to.be.true | ||
34 | |||
35 | await server.kill() | ||
36 | }) | ||
37 | |||
38 | it('Should enable open telemetry metrics', async function () { | ||
39 | this.timeout(120000) | ||
40 | |||
41 | await server.run({ | ||
42 | open_telemetry: { | ||
43 | metrics: { | ||
44 | enabled: true | ||
45 | } | ||
46 | } | ||
47 | }) | ||
48 | |||
49 | // Simulate a HTTP request | ||
50 | await server.videos.list() | ||
51 | |||
52 | const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
53 | expect(res.text).to.contain('peertube_job_queue_total{') | ||
54 | expect(res.text).to.contain('http_request_duration_ms_bucket{') | ||
55 | }) | ||
56 | |||
57 | it('Should have playback metrics', async function () { | ||
58 | await setAccessTokensToServers([ server ]) | ||
59 | |||
60 | const video = await server.videos.quickUpload({ name: 'video' }) | ||
61 | |||
62 | await server.metrics.addPlaybackMetric({ | ||
63 | metrics: { | ||
64 | playerMode: 'p2p-media-loader', | ||
65 | resolution: VideoResolution.H_1080P, | ||
66 | fps: 30, | ||
67 | resolutionChanges: 1, | ||
68 | errors: 2, | ||
69 | downloadedBytesP2P: 0, | ||
70 | downloadedBytesHTTP: 0, | ||
71 | uploadedBytesP2P: 5, | ||
72 | p2pPeers: 1, | ||
73 | p2pEnabled: false, | ||
74 | videoId: video.uuid | ||
75 | } | ||
76 | }) | ||
77 | |||
78 | const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
79 | |||
80 | expect(res.text).to.contain('peertube_playback_http_downloaded_bytes_total{') | ||
81 | expect(res.text).to.contain('peertube_playback_p2p_peers{') | ||
82 | expect(res.text).to.contain('p2pEnabled="false"') | ||
83 | }) | ||
84 | |||
85 | it('Should take the last playback metric', async function () { | ||
86 | await setAccessTokensToServers([ server ]) | ||
87 | |||
88 | const video = await server.videos.quickUpload({ name: 'video' }) | ||
89 | |||
90 | const metrics = { | ||
91 | playerMode: 'p2p-media-loader', | ||
92 | resolution: VideoResolution.H_1080P, | ||
93 | fps: 30, | ||
94 | resolutionChanges: 1, | ||
95 | errors: 2, | ||
96 | downloadedBytesP2P: 0, | ||
97 | downloadedBytesHTTP: 0, | ||
98 | uploadedBytesP2P: 5, | ||
99 | p2pPeers: 7, | ||
100 | p2pEnabled: false, | ||
101 | videoId: video.uuid | ||
102 | } as PlaybackMetricCreate | ||
103 | |||
104 | await server.metrics.addPlaybackMetric({ metrics }) | ||
105 | |||
106 | metrics.p2pPeers = 42 | ||
107 | await server.metrics.addPlaybackMetric({ metrics }) | ||
108 | |||
109 | const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
110 | |||
111 | // eslint-disable-next-line max-len | ||
112 | const label = `{videoOrigin="local",playerMode="p2p-media-loader",resolution="1080",fps="30",p2pEnabled="false",videoUUID="${video.uuid}"}` | ||
113 | expect(res.text).to.contain(`peertube_playback_p2p_peers${label} 42`) | ||
114 | expect(res.text).to.not.contain(`peertube_playback_p2p_peers${label} 7`) | ||
115 | }) | ||
116 | |||
117 | it('Should disable http request duration metrics', async function () { | ||
118 | await server.kill() | ||
119 | |||
120 | await server.run({ | ||
121 | open_telemetry: { | ||
122 | metrics: { | ||
123 | enabled: true, | ||
124 | http_request_duration: { | ||
125 | enabled: false | ||
126 | } | ||
127 | } | ||
128 | } | ||
129 | }) | ||
130 | |||
131 | // Simulate a HTTP request | ||
132 | await server.videos.list() | ||
133 | |||
134 | const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
135 | expect(res.text).to.not.contain('http_request_duration_ms_bucket{') | ||
136 | }) | ||
137 | |||
138 | after(async function () { | ||
139 | await server.kill() | ||
140 | }) | ||
141 | }) | ||
142 | |||
143 | describe('Tracing', function () { | ||
144 | let mockHTTP: MockHTTP | ||
145 | let mockPort: number | ||
146 | |||
147 | before(async function () { | ||
148 | mockHTTP = new MockHTTP() | ||
149 | mockPort = await mockHTTP.initialize() | ||
150 | }) | ||
151 | |||
152 | it('Should enable open telemetry tracing', async function () { | ||
153 | server = await createSingleServer(1) | ||
154 | |||
155 | await expectLogDoesNotContain(server, 'Registering Open Telemetry tracing') | ||
156 | |||
157 | await server.kill() | ||
158 | }) | ||
159 | |||
160 | it('Should enable open telemetry metrics', async function () { | ||
161 | await server.run({ | ||
162 | open_telemetry: { | ||
163 | tracing: { | ||
164 | enabled: true, | ||
165 | jaeger_exporter: { | ||
166 | endpoint: 'http://127.0.0.1:' + mockPort | ||
167 | } | ||
168 | } | ||
169 | } | ||
170 | }) | ||
171 | |||
172 | await expectLogContain(server, 'Registering Open Telemetry tracing') | ||
173 | }) | ||
174 | |||
175 | it('Should upload a video and correctly works', async function () { | ||
176 | await setAccessTokensToServers([ server ]) | ||
177 | |||
178 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PUBLIC }) | ||
179 | |||
180 | const video = await server.videos.get({ id: uuid }) | ||
181 | |||
182 | expect(video.name).to.equal('video') | ||
183 | }) | ||
184 | |||
185 | after(async function () { | ||
186 | await mockHTTP.terminate() | ||
187 | }) | ||
188 | }) | ||
189 | |||
190 | after(async function () { | ||
191 | await cleanupTests([ server ]) | ||
192 | }) | ||
193 | }) | ||
diff --git a/packages/tests/src/api/server/plugins.ts b/packages/tests/src/api/server/plugins.ts new file mode 100644 index 000000000..a78cea025 --- /dev/null +++ b/packages/tests/src/api/server/plugins.ts | |||
@@ -0,0 +1,410 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists, remove } from 'fs-extra/esm' | ||
5 | import { join } from 'path' | ||
6 | import { wait } from '@peertube/peertube-core-utils' | ||
7 | import { HttpStatusCode, PluginType } from '@peertube/peertube-models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | killallServers, | ||
12 | makeGetRequest, | ||
13 | PeerTubeServer, | ||
14 | PluginsCommand, | ||
15 | setAccessTokensToServers | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
18 | import { testHelloWorldRegisteredSettings } from '@tests/shared/plugins.js' | ||
19 | |||
20 | describe('Test plugins', function () { | ||
21 | let server: PeerTubeServer | ||
22 | let sqlCommand: SQLCommand | ||
23 | let command: PluginsCommand | ||
24 | |||
25 | before(async function () { | ||
26 | this.timeout(30000) | ||
27 | |||
28 | const configOverride = { | ||
29 | plugins: { | ||
30 | index: { check_latest_versions_interval: '5 seconds' } | ||
31 | } | ||
32 | } | ||
33 | server = await createSingleServer(1, configOverride) | ||
34 | await setAccessTokensToServers([ server ]) | ||
35 | |||
36 | command = server.plugins | ||
37 | |||
38 | sqlCommand = new SQLCommand(server) | ||
39 | }) | ||
40 | |||
41 | it('Should list and search available plugins and themes', async function () { | ||
42 | this.timeout(30000) | ||
43 | |||
44 | { | ||
45 | const body = await command.listAvailable({ | ||
46 | count: 1, | ||
47 | start: 0, | ||
48 | pluginType: PluginType.THEME, | ||
49 | search: 'background-red' | ||
50 | }) | ||
51 | |||
52 | expect(body.total).to.be.at.least(1) | ||
53 | expect(body.data).to.have.lengthOf(1) | ||
54 | } | ||
55 | |||
56 | { | ||
57 | const body1 = await command.listAvailable({ | ||
58 | count: 2, | ||
59 | start: 0, | ||
60 | sort: 'npmName' | ||
61 | }) | ||
62 | expect(body1.total).to.be.at.least(2) | ||
63 | |||
64 | const data1 = body1.data | ||
65 | expect(data1).to.have.lengthOf(2) | ||
66 | |||
67 | const body2 = await command.listAvailable({ | ||
68 | count: 2, | ||
69 | start: 0, | ||
70 | sort: '-npmName' | ||
71 | }) | ||
72 | expect(body2.total).to.be.at.least(2) | ||
73 | |||
74 | const data2 = body2.data | ||
75 | expect(data2).to.have.lengthOf(2) | ||
76 | |||
77 | expect(data1[0].npmName).to.not.equal(data2[0].npmName) | ||
78 | } | ||
79 | |||
80 | { | ||
81 | const body = await command.listAvailable({ | ||
82 | count: 10, | ||
83 | start: 0, | ||
84 | pluginType: PluginType.THEME, | ||
85 | search: 'background-red', | ||
86 | currentPeerTubeEngine: '1.0.0' | ||
87 | }) | ||
88 | |||
89 | const p = body.data.find(p => p.npmName === 'peertube-theme-background-red') | ||
90 | expect(p).to.be.undefined | ||
91 | } | ||
92 | }) | ||
93 | |||
94 | it('Should install a plugin and a theme', async function () { | ||
95 | this.timeout(30000) | ||
96 | |||
97 | await command.install({ npmName: 'peertube-plugin-hello-world' }) | ||
98 | await command.install({ npmName: 'peertube-theme-background-red' }) | ||
99 | }) | ||
100 | |||
101 | it('Should have the plugin loaded in the configuration', async function () { | ||
102 | for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) { | ||
103 | const theme = config.theme.registered.find(r => r.name === 'background-red') | ||
104 | expect(theme).to.not.be.undefined | ||
105 | expect(theme.npmName).to.equal('peertube-theme-background-red') | ||
106 | |||
107 | const plugin = config.plugin.registered.find(r => r.name === 'hello-world') | ||
108 | expect(plugin).to.not.be.undefined | ||
109 | expect(plugin.npmName).to.equal('peertube-plugin-hello-world') | ||
110 | } | ||
111 | }) | ||
112 | |||
113 | it('Should update the default theme in the configuration', async function () { | ||
114 | await server.config.updateCustomSubConfig({ | ||
115 | newConfig: { | ||
116 | theme: { default: 'background-red' } | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) { | ||
121 | expect(config.theme.default).to.equal('background-red') | ||
122 | } | ||
123 | }) | ||
124 | |||
125 | it('Should update my default theme', async function () { | ||
126 | await server.users.updateMe({ theme: 'background-red' }) | ||
127 | |||
128 | const user = await server.users.getMyInfo() | ||
129 | expect(user.theme).to.equal('background-red') | ||
130 | }) | ||
131 | |||
132 | it('Should list plugins and themes', async function () { | ||
133 | { | ||
134 | const body = await command.list({ | ||
135 | count: 1, | ||
136 | start: 0, | ||
137 | pluginType: PluginType.THEME | ||
138 | }) | ||
139 | expect(body.total).to.be.at.least(1) | ||
140 | |||
141 | const data = body.data | ||
142 | expect(data).to.have.lengthOf(1) | ||
143 | expect(data[0].name).to.equal('background-red') | ||
144 | } | ||
145 | |||
146 | { | ||
147 | const { data } = await command.list({ | ||
148 | count: 2, | ||
149 | start: 0, | ||
150 | sort: 'name' | ||
151 | }) | ||
152 | |||
153 | expect(data[0].name).to.equal('background-red') | ||
154 | expect(data[1].name).to.equal('hello-world') | ||
155 | } | ||
156 | |||
157 | { | ||
158 | const body = await command.list({ | ||
159 | count: 2, | ||
160 | start: 1, | ||
161 | sort: 'name' | ||
162 | }) | ||
163 | |||
164 | expect(body.data[0].name).to.equal('hello-world') | ||
165 | } | ||
166 | }) | ||
167 | |||
168 | it('Should get registered settings', async function () { | ||
169 | await testHelloWorldRegisteredSettings(server) | ||
170 | }) | ||
171 | |||
172 | it('Should get public settings', async function () { | ||
173 | const body = await command.getPublicSettings({ npmName: 'peertube-plugin-hello-world' }) | ||
174 | const publicSettings = body.publicSettings | ||
175 | |||
176 | expect(Object.keys(publicSettings)).to.have.lengthOf(1) | ||
177 | expect(Object.keys(publicSettings)).to.deep.equal([ 'user-name' ]) | ||
178 | expect(publicSettings['user-name']).to.be.null | ||
179 | }) | ||
180 | |||
181 | it('Should update the settings', async function () { | ||
182 | const settings = { | ||
183 | 'admin-name': 'Cid' | ||
184 | } | ||
185 | |||
186 | await command.updateSettings({ | ||
187 | npmName: 'peertube-plugin-hello-world', | ||
188 | settings | ||
189 | }) | ||
190 | }) | ||
191 | |||
192 | it('Should have watched settings changes', async function () { | ||
193 | await server.servers.waitUntilLog('Settings changed!') | ||
194 | }) | ||
195 | |||
196 | it('Should get a plugin and a theme', async function () { | ||
197 | { | ||
198 | const plugin = await command.get({ npmName: 'peertube-plugin-hello-world' }) | ||
199 | |||
200 | expect(plugin.type).to.equal(PluginType.PLUGIN) | ||
201 | expect(plugin.name).to.equal('hello-world') | ||
202 | expect(plugin.description).to.exist | ||
203 | expect(plugin.homepage).to.exist | ||
204 | expect(plugin.uninstalled).to.be.false | ||
205 | expect(plugin.enabled).to.be.true | ||
206 | expect(plugin.description).to.exist | ||
207 | expect(plugin.version).to.exist | ||
208 | expect(plugin.peertubeEngine).to.exist | ||
209 | expect(plugin.createdAt).to.exist | ||
210 | |||
211 | expect(plugin.settings).to.not.be.undefined | ||
212 | expect(plugin.settings['admin-name']).to.equal('Cid') | ||
213 | } | ||
214 | |||
215 | { | ||
216 | const plugin = await command.get({ npmName: 'peertube-theme-background-red' }) | ||
217 | |||
218 | expect(plugin.type).to.equal(PluginType.THEME) | ||
219 | expect(plugin.name).to.equal('background-red') | ||
220 | expect(plugin.description).to.exist | ||
221 | expect(plugin.homepage).to.exist | ||
222 | expect(plugin.uninstalled).to.be.false | ||
223 | expect(plugin.enabled).to.be.true | ||
224 | expect(plugin.description).to.exist | ||
225 | expect(plugin.version).to.exist | ||
226 | expect(plugin.peertubeEngine).to.exist | ||
227 | expect(plugin.createdAt).to.exist | ||
228 | |||
229 | expect(plugin.settings).to.be.null | ||
230 | } | ||
231 | }) | ||
232 | |||
233 | it('Should update the plugin and the theme', async function () { | ||
234 | this.timeout(180000) | ||
235 | |||
236 | // Wait the scheduler that get the latest plugins versions | ||
237 | await wait(6000) | ||
238 | |||
239 | async function testUpdate (type: 'plugin' | 'theme', name: string) { | ||
240 | // Fake update our plugin version | ||
241 | await sqlCommand.setPluginVersion(name, '0.0.1') | ||
242 | |||
243 | // Fake update package.json | ||
244 | const packageJSON = await command.getPackageJSON(`peertube-${type}-${name}`) | ||
245 | const oldVersion = packageJSON.version | ||
246 | |||
247 | packageJSON.version = '0.0.1' | ||
248 | await command.updatePackageJSON(`peertube-${type}-${name}`, packageJSON) | ||
249 | |||
250 | // Restart the server to take into account this change | ||
251 | await killallServers([ server ]) | ||
252 | await server.run() | ||
253 | |||
254 | const checkConfig = async (version: string) => { | ||
255 | for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) { | ||
256 | expect(config[type].registered.find(r => r.name === name).version).to.equal(version) | ||
257 | } | ||
258 | } | ||
259 | |||
260 | const getPluginFromAPI = async () => { | ||
261 | const body = await command.list({ pluginType: type === 'plugin' ? PluginType.PLUGIN : PluginType.THEME }) | ||
262 | |||
263 | return body.data.find(p => p.name === name) | ||
264 | } | ||
265 | |||
266 | { | ||
267 | const plugin = await getPluginFromAPI() | ||
268 | expect(plugin.version).to.equal('0.0.1') | ||
269 | expect(plugin.latestVersion).to.exist | ||
270 | expect(plugin.latestVersion).to.not.equal('0.0.1') | ||
271 | |||
272 | await checkConfig('0.0.1') | ||
273 | } | ||
274 | |||
275 | { | ||
276 | await command.update({ npmName: `peertube-${type}-${name}` }) | ||
277 | |||
278 | const plugin = await getPluginFromAPI() | ||
279 | expect(plugin.version).to.equal(oldVersion) | ||
280 | |||
281 | const updatedPackageJSON = await command.getPackageJSON(`peertube-${type}-${name}`) | ||
282 | expect(updatedPackageJSON.version).to.equal(oldVersion) | ||
283 | |||
284 | await checkConfig(oldVersion) | ||
285 | } | ||
286 | } | ||
287 | |||
288 | await testUpdate('theme', 'background-red') | ||
289 | await testUpdate('plugin', 'hello-world') | ||
290 | }) | ||
291 | |||
292 | it('Should uninstall the plugin', async function () { | ||
293 | await command.uninstall({ npmName: 'peertube-plugin-hello-world' }) | ||
294 | |||
295 | const body = await command.list({ pluginType: PluginType.PLUGIN }) | ||
296 | expect(body.total).to.equal(0) | ||
297 | expect(body.data).to.have.lengthOf(0) | ||
298 | }) | ||
299 | |||
300 | it('Should list uninstalled plugins', async function () { | ||
301 | const body = await command.list({ pluginType: PluginType.PLUGIN, uninstalled: true }) | ||
302 | expect(body.total).to.equal(1) | ||
303 | expect(body.data).to.have.lengthOf(1) | ||
304 | |||
305 | const plugin = body.data[0] | ||
306 | expect(plugin.name).to.equal('hello-world') | ||
307 | expect(plugin.enabled).to.be.false | ||
308 | expect(plugin.uninstalled).to.be.true | ||
309 | }) | ||
310 | |||
311 | it('Should uninstall the theme', async function () { | ||
312 | await command.uninstall({ npmName: 'peertube-theme-background-red' }) | ||
313 | }) | ||
314 | |||
315 | it('Should have updated the configuration', async function () { | ||
316 | for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) { | ||
317 | expect(config.theme.default).to.equal('default') | ||
318 | |||
319 | const theme = config.theme.registered.find(r => r.name === 'background-red') | ||
320 | expect(theme).to.be.undefined | ||
321 | |||
322 | const plugin = config.plugin.registered.find(r => r.name === 'hello-world') | ||
323 | expect(plugin).to.be.undefined | ||
324 | } | ||
325 | }) | ||
326 | |||
327 | it('Should have updated the user theme', async function () { | ||
328 | const user = await server.users.getMyInfo() | ||
329 | expect(user.theme).to.equal('instance-default') | ||
330 | }) | ||
331 | |||
332 | it('Should not install a broken plugin', async function () { | ||
333 | this.timeout(60000) | ||
334 | |||
335 | async function check () { | ||
336 | const body = await command.list({ pluginType: PluginType.PLUGIN }) | ||
337 | const plugins = body.data | ||
338 | expect(plugins.find(p => p.name === 'test-broken')).to.not.exist | ||
339 | } | ||
340 | |||
341 | await command.install({ | ||
342 | path: PluginsCommand.getPluginTestPath('-broken'), | ||
343 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
344 | }) | ||
345 | |||
346 | await check() | ||
347 | |||
348 | await killallServers([ server ]) | ||
349 | await server.run() | ||
350 | |||
351 | await check() | ||
352 | }) | ||
353 | |||
354 | it('Should rebuild native modules on Node ABI change', async function () { | ||
355 | this.timeout(60000) | ||
356 | |||
357 | const removeNativeModule = async () => { | ||
358 | await remove(join(baseNativeModule, 'build')) | ||
359 | await remove(join(baseNativeModule, 'prebuilds')) | ||
360 | } | ||
361 | |||
362 | await command.install({ path: PluginsCommand.getPluginTestPath('-native') }) | ||
363 | |||
364 | await makeGetRequest({ | ||
365 | url: server.url, | ||
366 | path: '/plugins/test-native/router', | ||
367 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
368 | }) | ||
369 | |||
370 | const query = `UPDATE "application" SET "nodeABIVersion" = 1` | ||
371 | await sqlCommand.updateQuery(query) | ||
372 | |||
373 | const baseNativeModule = server.servers.buildDirectory(join('plugins', 'node_modules', 'a-native-example')) | ||
374 | |||
375 | await removeNativeModule() | ||
376 | await server.kill() | ||
377 | await server.run() | ||
378 | |||
379 | await wait(3000) | ||
380 | |||
381 | expect(await pathExists(join(baseNativeModule, 'build'))).to.be.true | ||
382 | expect(await pathExists(join(baseNativeModule, 'prebuilds'))).to.be.true | ||
383 | |||
384 | await makeGetRequest({ | ||
385 | url: server.url, | ||
386 | path: '/plugins/test-native/router', | ||
387 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
388 | }) | ||
389 | |||
390 | await removeNativeModule() | ||
391 | |||
392 | await server.kill() | ||
393 | await server.run() | ||
394 | |||
395 | expect(await pathExists(join(baseNativeModule, 'build'))).to.be.false | ||
396 | expect(await pathExists(join(baseNativeModule, 'prebuilds'))).to.be.false | ||
397 | |||
398 | await makeGetRequest({ | ||
399 | url: server.url, | ||
400 | path: '/plugins/test-native/router', | ||
401 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
402 | }) | ||
403 | }) | ||
404 | |||
405 | after(async function () { | ||
406 | await sqlCommand.cleanup() | ||
407 | |||
408 | await cleanupTests([ server ]) | ||
409 | }) | ||
410 | }) | ||
diff --git a/packages/tests/src/api/server/proxy.ts b/packages/tests/src/api/server/proxy.ts new file mode 100644 index 000000000..c7d13f4ab --- /dev/null +++ b/packages/tests/src/api/server/proxy.ts | |||
@@ -0,0 +1,173 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode, HttpStatusCodeType, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
17 | import { expectStartWith, expectNotStartWith } from '@tests/shared/checks.js' | ||
18 | import { MockProxy } from '@tests/shared/mock-servers/mock-proxy.js' | ||
19 | |||
20 | describe('Test proxy', function () { | ||
21 | let servers: PeerTubeServer[] = [] | ||
22 | let proxy: MockProxy | ||
23 | |||
24 | const goodEnv = { HTTP_PROXY: '' } | ||
25 | const badEnv = { HTTP_PROXY: 'http://127.0.0.1:9000' } | ||
26 | |||
27 | before(async function () { | ||
28 | this.timeout(120000) | ||
29 | |||
30 | proxy = new MockProxy() | ||
31 | |||
32 | const proxyPort = await proxy.initialize() | ||
33 | servers = await createMultipleServers(2) | ||
34 | |||
35 | goodEnv.HTTP_PROXY = 'http://127.0.0.1:' + proxyPort | ||
36 | |||
37 | await setAccessTokensToServers(servers) | ||
38 | await setDefaultVideoChannel(servers) | ||
39 | await doubleFollow(servers[0], servers[1]) | ||
40 | }) | ||
41 | |||
42 | describe('Federation', function () { | ||
43 | |||
44 | it('Should succeed federation with the appropriate proxy config', async function () { | ||
45 | this.timeout(40000) | ||
46 | |||
47 | await servers[0].kill() | ||
48 | await servers[0].run({}, { env: goodEnv }) | ||
49 | |||
50 | await servers[0].videos.quickUpload({ name: 'video 1' }) | ||
51 | |||
52 | await waitJobs(servers) | ||
53 | |||
54 | for (const server of servers) { | ||
55 | const { total, data } = await server.videos.list() | ||
56 | expect(total).to.equal(1) | ||
57 | expect(data).to.have.lengthOf(1) | ||
58 | } | ||
59 | }) | ||
60 | |||
61 | it('Should fail federation with a wrong proxy config', async function () { | ||
62 | this.timeout(40000) | ||
63 | |||
64 | await servers[0].kill() | ||
65 | await servers[0].run({}, { env: badEnv }) | ||
66 | |||
67 | await servers[0].videos.quickUpload({ name: 'video 2' }) | ||
68 | |||
69 | await waitJobs(servers) | ||
70 | |||
71 | { | ||
72 | const { total, data } = await servers[0].videos.list() | ||
73 | expect(total).to.equal(2) | ||
74 | expect(data).to.have.lengthOf(2) | ||
75 | } | ||
76 | |||
77 | { | ||
78 | const { total, data } = await servers[1].videos.list() | ||
79 | expect(total).to.equal(1) | ||
80 | expect(data).to.have.lengthOf(1) | ||
81 | } | ||
82 | }) | ||
83 | }) | ||
84 | |||
85 | describe('Videos import', async function () { | ||
86 | |||
87 | function quickImport (expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) { | ||
88 | return servers[0].imports.importVideo({ | ||
89 | attributes: { | ||
90 | name: 'video import', | ||
91 | channelId: servers[0].store.channel.id, | ||
92 | privacy: VideoPrivacy.PUBLIC, | ||
93 | targetUrl: FIXTURE_URLS.peertube_long | ||
94 | }, | ||
95 | expectedStatus | ||
96 | }) | ||
97 | } | ||
98 | |||
99 | it('Should succeed import with the appropriate proxy config', async function () { | ||
100 | this.timeout(240000) | ||
101 | |||
102 | await servers[0].kill() | ||
103 | await servers[0].run({}, { env: goodEnv }) | ||
104 | |||
105 | await quickImport() | ||
106 | |||
107 | await waitJobs(servers) | ||
108 | |||
109 | const { total, data } = await servers[0].videos.list() | ||
110 | expect(total).to.equal(3) | ||
111 | expect(data).to.have.lengthOf(3) | ||
112 | }) | ||
113 | |||
114 | it('Should fail import with a wrong proxy config', async function () { | ||
115 | this.timeout(120000) | ||
116 | |||
117 | await servers[0].kill() | ||
118 | await servers[0].run({}, { env: badEnv }) | ||
119 | |||
120 | await quickImport(HttpStatusCode.BAD_REQUEST_400) | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | describe('Object storage', function () { | ||
125 | if (areMockObjectStorageTestsDisabled()) return | ||
126 | |||
127 | const objectStorage = new ObjectStorageCommand() | ||
128 | |||
129 | before(async function () { | ||
130 | this.timeout(30000) | ||
131 | |||
132 | await objectStorage.prepareDefaultMockBuckets() | ||
133 | }) | ||
134 | |||
135 | it('Should succeed to upload to object storage with the appropriate proxy config', async function () { | ||
136 | this.timeout(120000) | ||
137 | |||
138 | await servers[0].kill() | ||
139 | await servers[0].run(objectStorage.getDefaultMockConfig(), { env: goodEnv }) | ||
140 | |||
141 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
142 | await waitJobs(servers) | ||
143 | |||
144 | const video = await servers[0].videos.get({ id: uuid }) | ||
145 | |||
146 | expectStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
147 | }) | ||
148 | |||
149 | it('Should fail to upload to object storage with a wrong proxy config', async function () { | ||
150 | this.timeout(120000) | ||
151 | |||
152 | await servers[0].kill() | ||
153 | await servers[0].run(objectStorage.getDefaultMockConfig(), { env: badEnv }) | ||
154 | |||
155 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
156 | await waitJobs(servers, { skipDelayed: true }) | ||
157 | |||
158 | const video = await servers[0].videos.get({ id: uuid }) | ||
159 | |||
160 | expectNotStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
161 | }) | ||
162 | |||
163 | after(async function () { | ||
164 | await objectStorage.cleanupMock() | ||
165 | }) | ||
166 | }) | ||
167 | |||
168 | after(async function () { | ||
169 | await proxy.terminate() | ||
170 | |||
171 | await cleanupTests(servers) | ||
172 | }) | ||
173 | }) | ||
diff --git a/packages/tests/src/api/server/reverse-proxy.ts b/packages/tests/src/api/server/reverse-proxy.ts new file mode 100644 index 000000000..7e334cc3e --- /dev/null +++ b/packages/tests/src/api/server/reverse-proxy.ts | |||
@@ -0,0 +1,156 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
7 | |||
8 | describe('Test application behind a reverse proxy', function () { | ||
9 | let server: PeerTubeServer | ||
10 | let userAccessToken: string | ||
11 | let videoId: string | ||
12 | |||
13 | before(async function () { | ||
14 | this.timeout(60000) | ||
15 | |||
16 | const config = { | ||
17 | rates_limit: { | ||
18 | api: { | ||
19 | max: 50, | ||
20 | window: 5000 | ||
21 | }, | ||
22 | signup: { | ||
23 | max: 3, | ||
24 | window: 5000 | ||
25 | }, | ||
26 | login: { | ||
27 | max: 20 | ||
28 | } | ||
29 | }, | ||
30 | signup: { | ||
31 | limit: 20 | ||
32 | } | ||
33 | } | ||
34 | |||
35 | server = await createSingleServer(1, config) | ||
36 | await setAccessTokensToServers([ server ]) | ||
37 | |||
38 | userAccessToken = await server.users.generateUserAndToken('user') | ||
39 | |||
40 | const { uuid } = await server.videos.upload() | ||
41 | videoId = uuid | ||
42 | }) | ||
43 | |||
44 | it('Should view a video only once with the same IP by default', async function () { | ||
45 | this.timeout(40000) | ||
46 | |||
47 | await server.views.simulateView({ id: videoId }) | ||
48 | await server.views.simulateView({ id: videoId }) | ||
49 | |||
50 | // Wait the repeatable job | ||
51 | await wait(8000) | ||
52 | |||
53 | const video = await server.videos.get({ id: videoId }) | ||
54 | expect(video.views).to.equal(1) | ||
55 | }) | ||
56 | |||
57 | it('Should view a video 2 times with the X-Forwarded-For header set', async function () { | ||
58 | this.timeout(20000) | ||
59 | |||
60 | await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.1,127.0.0.1' }) | ||
61 | await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.2,127.0.0.1' }) | ||
62 | |||
63 | // Wait the repeatable job | ||
64 | await wait(8000) | ||
65 | |||
66 | const video = await server.videos.get({ id: videoId }) | ||
67 | expect(video.views).to.equal(3) | ||
68 | }) | ||
69 | |||
70 | it('Should view a video only once with the same client IP in the X-Forwarded-For header', async function () { | ||
71 | this.timeout(20000) | ||
72 | |||
73 | await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.4,0.0.0.3,::ffff:127.0.0.1' }) | ||
74 | await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.5,0.0.0.3,127.0.0.1' }) | ||
75 | |||
76 | // Wait the repeatable job | ||
77 | await wait(8000) | ||
78 | |||
79 | const video = await server.videos.get({ id: videoId }) | ||
80 | expect(video.views).to.equal(4) | ||
81 | }) | ||
82 | |||
83 | it('Should view a video two times with a different client IP in the X-Forwarded-For header', async function () { | ||
84 | this.timeout(20000) | ||
85 | |||
86 | await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.6,127.0.0.1' }) | ||
87 | await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.7,127.0.0.1' }) | ||
88 | |||
89 | // Wait the repeatable job | ||
90 | await wait(8000) | ||
91 | |||
92 | const video = await server.videos.get({ id: videoId }) | ||
93 | expect(video.views).to.equal(6) | ||
94 | }) | ||
95 | |||
96 | it('Should rate limit logins', async function () { | ||
97 | const user = { username: 'root', password: 'fail' } | ||
98 | |||
99 | for (let i = 0; i < 18; i++) { | ||
100 | await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
101 | } | ||
102 | |||
103 | await server.login.login({ user, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
104 | }) | ||
105 | |||
106 | it('Should rate limit signup', async function () { | ||
107 | for (let i = 0; i < 10; i++) { | ||
108 | try { | ||
109 | await server.registrations.register({ username: 'test' + i }) | ||
110 | } catch { | ||
111 | // empty | ||
112 | } | ||
113 | } | ||
114 | |||
115 | await server.registrations.register({ username: 'test42', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
116 | }) | ||
117 | |||
118 | it('Should not rate limit failed signup', async function () { | ||
119 | this.timeout(30000) | ||
120 | |||
121 | await wait(7000) | ||
122 | |||
123 | for (let i = 0; i < 3; i++) { | ||
124 | await server.registrations.register({ username: 'test' + i, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
125 | } | ||
126 | |||
127 | await server.registrations.register({ username: 'test43', expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | ||
128 | |||
129 | }) | ||
130 | |||
131 | it('Should rate limit API calls', async function () { | ||
132 | this.timeout(30000) | ||
133 | |||
134 | await wait(7000) | ||
135 | |||
136 | for (let i = 0; i < 100; i++) { | ||
137 | try { | ||
138 | await server.videos.get({ id: videoId }) | ||
139 | } catch { | ||
140 | // don't care if it fails | ||
141 | } | ||
142 | } | ||
143 | |||
144 | await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
145 | }) | ||
146 | |||
147 | it('Should rate limit API calls with a user but not with an admin', async function () { | ||
148 | await server.videos.get({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) | ||
149 | |||
150 | await server.videos.get({ id: videoId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
151 | }) | ||
152 | |||
153 | after(async function () { | ||
154 | await cleanupTests([ server ]) | ||
155 | }) | ||
156 | }) | ||
diff --git a/packages/tests/src/api/server/services.ts b/packages/tests/src/api/server/services.ts new file mode 100644 index 000000000..349d29a58 --- /dev/null +++ b/packages/tests/src/api/server/services.ts | |||
@@ -0,0 +1,143 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { Video, VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test services', function () { | ||
14 | let server: PeerTubeServer = null | ||
15 | let playlistUUID: string | ||
16 | let playlistDisplayName: string | ||
17 | let video: Video | ||
18 | |||
19 | const urlSuffixes = [ | ||
20 | { | ||
21 | input: '', | ||
22 | output: '' | ||
23 | }, | ||
24 | { | ||
25 | input: '?param=1', | ||
26 | output: '' | ||
27 | }, | ||
28 | { | ||
29 | input: '?muted=1&warningTitle=0&toto=1', | ||
30 | output: '?muted=1&warningTitle=0' | ||
31 | } | ||
32 | ] | ||
33 | |||
34 | before(async function () { | ||
35 | this.timeout(120000) | ||
36 | |||
37 | server = await createSingleServer(1) | ||
38 | |||
39 | await setAccessTokensToServers([ server ]) | ||
40 | await setDefaultVideoChannel([ server ]) | ||
41 | |||
42 | { | ||
43 | const attributes = { name: 'my super name' } | ||
44 | await server.videos.upload({ attributes }) | ||
45 | |||
46 | const { data } = await server.videos.list() | ||
47 | video = data[0] | ||
48 | } | ||
49 | |||
50 | { | ||
51 | const created = await server.playlists.create({ | ||
52 | attributes: { | ||
53 | displayName: 'The Life and Times of Scrooge McDuck', | ||
54 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
55 | videoChannelId: server.store.channel.id | ||
56 | } | ||
57 | }) | ||
58 | |||
59 | playlistUUID = created.uuid | ||
60 | playlistDisplayName = 'The Life and Times of Scrooge McDuck' | ||
61 | |||
62 | await server.playlists.addElement({ | ||
63 | playlistId: created.id, | ||
64 | attributes: { | ||
65 | videoId: video.id | ||
66 | } | ||
67 | }) | ||
68 | } | ||
69 | }) | ||
70 | |||
71 | it('Should have a valid oEmbed video response', async function () { | ||
72 | for (const basePath of [ '/videos/watch/', '/w/' ]) { | ||
73 | for (const suffix of urlSuffixes) { | ||
74 | const oembedUrl = server.url + basePath + video.uuid + suffix.input | ||
75 | |||
76 | const res = await server.services.getOEmbed({ oembedUrl }) | ||
77 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' + | ||
78 | `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}${suffix.output}" ` + | ||
79 | 'frameborder="0" allowfullscreen></iframe>' | ||
80 | |||
81 | const expectedThumbnailUrl = 'http://' + server.host + video.previewPath | ||
82 | |||
83 | expect(res.body.html).to.equal(expectedHtml) | ||
84 | expect(res.body.title).to.equal(video.name) | ||
85 | expect(res.body.author_name).to.equal(server.store.channel.displayName) | ||
86 | expect(res.body.width).to.equal(560) | ||
87 | expect(res.body.height).to.equal(315) | ||
88 | expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) | ||
89 | expect(res.body.thumbnail_width).to.equal(850) | ||
90 | expect(res.body.thumbnail_height).to.equal(480) | ||
91 | } | ||
92 | } | ||
93 | }) | ||
94 | |||
95 | it('Should have a valid playlist oEmbed response', async function () { | ||
96 | for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) { | ||
97 | for (const suffix of urlSuffixes) { | ||
98 | const oembedUrl = server.url + basePath + playlistUUID + suffix.input | ||
99 | |||
100 | const res = await server.services.getOEmbed({ oembedUrl }) | ||
101 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' + | ||
102 | `title="${playlistDisplayName}" src="http://${server.host}/video-playlists/embed/${playlistUUID}${suffix.output}" ` + | ||
103 | 'frameborder="0" allowfullscreen></iframe>' | ||
104 | |||
105 | expect(res.body.html).to.equal(expectedHtml) | ||
106 | expect(res.body.title).to.equal('The Life and Times of Scrooge McDuck') | ||
107 | expect(res.body.author_name).to.equal(server.store.channel.displayName) | ||
108 | expect(res.body.width).to.equal(560) | ||
109 | expect(res.body.height).to.equal(315) | ||
110 | expect(res.body.thumbnail_url).exist | ||
111 | expect(res.body.thumbnail_width).to.equal(280) | ||
112 | expect(res.body.thumbnail_height).to.equal(157) | ||
113 | } | ||
114 | } | ||
115 | }) | ||
116 | |||
117 | it('Should have a valid oEmbed response with small max height query', async function () { | ||
118 | for (const basePath of [ '/videos/watch/', '/w/' ]) { | ||
119 | const oembedUrl = 'http://' + server.host + basePath + video.uuid | ||
120 | const format = 'json' | ||
121 | const maxHeight = 50 | ||
122 | const maxWidth = 50 | ||
123 | |||
124 | const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth }) | ||
125 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts allow-popups" ' + | ||
126 | `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}" ` + | ||
127 | 'frameborder="0" allowfullscreen></iframe>' | ||
128 | |||
129 | expect(res.body.html).to.equal(expectedHtml) | ||
130 | expect(res.body.title).to.equal(video.name) | ||
131 | expect(res.body.author_name).to.equal(server.store.channel.displayName) | ||
132 | expect(res.body.height).to.equal(50) | ||
133 | expect(res.body.width).to.equal(50) | ||
134 | expect(res.body).to.not.have.property('thumbnail_url') | ||
135 | expect(res.body).to.not.have.property('thumbnail_width') | ||
136 | expect(res.body).to.not.have.property('thumbnail_height') | ||
137 | } | ||
138 | }) | ||
139 | |||
140 | after(async function () { | ||
141 | await cleanupTests([ server ]) | ||
142 | }) | ||
143 | }) | ||
diff --git a/packages/tests/src/api/server/slow-follows.ts b/packages/tests/src/api/server/slow-follows.ts new file mode 100644 index 000000000..d03109001 --- /dev/null +++ b/packages/tests/src/api/server/slow-follows.ts | |||
@@ -0,0 +1,85 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { Job } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test slow follows', function () { | ||
15 | let servers: PeerTubeServer[] = [] | ||
16 | |||
17 | let afterFollows: Date | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(120000) | ||
21 | |||
22 | servers = await createMultipleServers(3) | ||
23 | |||
24 | // Get the access tokens | ||
25 | await setAccessTokensToServers(servers) | ||
26 | |||
27 | await doubleFollow(servers[0], servers[1]) | ||
28 | await doubleFollow(servers[0], servers[2]) | ||
29 | |||
30 | afterFollows = new Date() | ||
31 | |||
32 | for (let i = 0; i < 5; i++) { | ||
33 | await servers[0].videos.quickUpload({ name: 'video ' + i }) | ||
34 | } | ||
35 | |||
36 | await waitJobs(servers) | ||
37 | }) | ||
38 | |||
39 | it('Should only have broadcast jobs', async function () { | ||
40 | const { data } = await servers[0].jobs.list({ jobType: 'activitypub-http-unicast', sort: '-createdAt' }) | ||
41 | |||
42 | for (const job of data) { | ||
43 | expect(new Date(job.createdAt)).below(afterFollows) | ||
44 | } | ||
45 | }) | ||
46 | |||
47 | it('Should process bad follower', async function () { | ||
48 | this.timeout(30000) | ||
49 | |||
50 | await servers[1].kill() | ||
51 | |||
52 | // Set server 2 as bad follower | ||
53 | await servers[0].videos.quickUpload({ name: 'video 6' }) | ||
54 | await waitJobs(servers[0]) | ||
55 | |||
56 | afterFollows = new Date() | ||
57 | const filter = (job: Job) => new Date(job.createdAt) > afterFollows | ||
58 | |||
59 | // Resend another broadcast job | ||
60 | await servers[0].videos.quickUpload({ name: 'video 7' }) | ||
61 | await waitJobs(servers[0]) | ||
62 | |||
63 | const resBroadcast = await servers[0].jobs.list({ jobType: 'activitypub-http-broadcast', sort: '-createdAt' }) | ||
64 | const resUnicast = await servers[0].jobs.list({ jobType: 'activitypub-http-unicast', sort: '-createdAt' }) | ||
65 | |||
66 | const broadcast = resBroadcast.data.filter(filter) | ||
67 | const unicast = resUnicast.data.filter(filter) | ||
68 | |||
69 | expect(unicast).to.have.lengthOf(2) | ||
70 | expect(broadcast).to.have.lengthOf(2) | ||
71 | |||
72 | for (const u of unicast) { | ||
73 | expect(u.data.uri).to.equal(servers[1].url + '/inbox') | ||
74 | } | ||
75 | |||
76 | for (const b of broadcast) { | ||
77 | expect(b.data.uris).to.have.lengthOf(1) | ||
78 | expect(b.data.uris[0]).to.equal(servers[2].url + '/inbox') | ||
79 | } | ||
80 | }) | ||
81 | |||
82 | after(async function () { | ||
83 | await cleanupTests(servers) | ||
84 | }) | ||
85 | }) | ||
diff --git a/packages/tests/src/api/server/stats.ts b/packages/tests/src/api/server/stats.ts new file mode 100644 index 000000000..32ab323ce --- /dev/null +++ b/packages/tests/src/api/server/stats.ts | |||
@@ -0,0 +1,279 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { ActivityType, VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test stats (excluding redundancy)', function () { | ||
18 | let servers: PeerTubeServer[] = [] | ||
19 | let channelId | ||
20 | const user = { | ||
21 | username: 'user1', | ||
22 | password: 'super_password' | ||
23 | } | ||
24 | |||
25 | before(async function () { | ||
26 | this.timeout(120000) | ||
27 | |||
28 | servers = await createMultipleServers(3) | ||
29 | |||
30 | await setAccessTokensToServers(servers) | ||
31 | await setDefaultChannelAvatar(servers) | ||
32 | await setDefaultAccountAvatar(servers) | ||
33 | |||
34 | await doubleFollow(servers[0], servers[1]) | ||
35 | |||
36 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
37 | |||
38 | const { uuid } = await servers[0].videos.upload({ attributes: { fixture: 'video_short.webm' } }) | ||
39 | |||
40 | await servers[0].comments.createThread({ videoId: uuid, text: 'comment' }) | ||
41 | |||
42 | await servers[0].views.simulateView({ id: uuid }) | ||
43 | |||
44 | // Wait the video views repeatable job | ||
45 | await wait(8000) | ||
46 | |||
47 | await servers[2].follows.follow({ hosts: [ servers[0].url ] }) | ||
48 | await waitJobs(servers) | ||
49 | }) | ||
50 | |||
51 | it('Should have the correct stats on instance 1', async function () { | ||
52 | const data = await servers[0].stats.get() | ||
53 | |||
54 | expect(data.totalLocalVideoComments).to.equal(1) | ||
55 | expect(data.totalLocalVideos).to.equal(1) | ||
56 | expect(data.totalLocalVideoViews).to.equal(1) | ||
57 | expect(data.totalLocalVideoFilesSize).to.equal(218910) | ||
58 | expect(data.totalUsers).to.equal(2) | ||
59 | expect(data.totalVideoComments).to.equal(1) | ||
60 | expect(data.totalVideos).to.equal(1) | ||
61 | expect(data.totalInstanceFollowers).to.equal(2) | ||
62 | expect(data.totalInstanceFollowing).to.equal(1) | ||
63 | expect(data.totalLocalPlaylists).to.equal(0) | ||
64 | }) | ||
65 | |||
66 | it('Should have the correct stats on instance 2', async function () { | ||
67 | const data = await servers[1].stats.get() | ||
68 | |||
69 | expect(data.totalLocalVideoComments).to.equal(0) | ||
70 | expect(data.totalLocalVideos).to.equal(0) | ||
71 | expect(data.totalLocalVideoViews).to.equal(0) | ||
72 | expect(data.totalLocalVideoFilesSize).to.equal(0) | ||
73 | expect(data.totalUsers).to.equal(1) | ||
74 | expect(data.totalVideoComments).to.equal(1) | ||
75 | expect(data.totalVideos).to.equal(1) | ||
76 | expect(data.totalInstanceFollowers).to.equal(1) | ||
77 | expect(data.totalInstanceFollowing).to.equal(1) | ||
78 | expect(data.totalLocalPlaylists).to.equal(0) | ||
79 | }) | ||
80 | |||
81 | it('Should have the correct stats on instance 3', async function () { | ||
82 | const data = await servers[2].stats.get() | ||
83 | |||
84 | expect(data.totalLocalVideoComments).to.equal(0) | ||
85 | expect(data.totalLocalVideos).to.equal(0) | ||
86 | expect(data.totalLocalVideoViews).to.equal(0) | ||
87 | expect(data.totalUsers).to.equal(1) | ||
88 | expect(data.totalVideoComments).to.equal(1) | ||
89 | expect(data.totalVideos).to.equal(1) | ||
90 | expect(data.totalInstanceFollowing).to.equal(1) | ||
91 | expect(data.totalInstanceFollowers).to.equal(0) | ||
92 | expect(data.totalLocalPlaylists).to.equal(0) | ||
93 | }) | ||
94 | |||
95 | it('Should have the correct total videos stats after an unfollow', async function () { | ||
96 | this.timeout(15000) | ||
97 | |||
98 | await servers[2].follows.unfollow({ target: servers[0] }) | ||
99 | await waitJobs(servers) | ||
100 | |||
101 | const data = await servers[2].stats.get() | ||
102 | |||
103 | expect(data.totalVideos).to.equal(0) | ||
104 | }) | ||
105 | |||
106 | it('Should have the correct active user stats', async function () { | ||
107 | const server = servers[0] | ||
108 | |||
109 | { | ||
110 | const data = await server.stats.get() | ||
111 | |||
112 | expect(data.totalDailyActiveUsers).to.equal(1) | ||
113 | expect(data.totalWeeklyActiveUsers).to.equal(1) | ||
114 | expect(data.totalMonthlyActiveUsers).to.equal(1) | ||
115 | } | ||
116 | |||
117 | { | ||
118 | await server.login.getAccessToken(user) | ||
119 | |||
120 | const data = await server.stats.get() | ||
121 | |||
122 | expect(data.totalDailyActiveUsers).to.equal(2) | ||
123 | expect(data.totalWeeklyActiveUsers).to.equal(2) | ||
124 | expect(data.totalMonthlyActiveUsers).to.equal(2) | ||
125 | } | ||
126 | }) | ||
127 | |||
128 | it('Should have the correct active channel stats', async function () { | ||
129 | const server = servers[0] | ||
130 | |||
131 | { | ||
132 | const data = await server.stats.get() | ||
133 | |||
134 | expect(data.totalLocalVideoChannels).to.equal(2) | ||
135 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(1) | ||
136 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1) | ||
137 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1) | ||
138 | } | ||
139 | |||
140 | { | ||
141 | const attributes = { | ||
142 | name: 'stats_channel', | ||
143 | displayName: 'My stats channel' | ||
144 | } | ||
145 | const created = await server.channels.create({ attributes }) | ||
146 | channelId = created.id | ||
147 | |||
148 | const data = await server.stats.get() | ||
149 | |||
150 | expect(data.totalLocalVideoChannels).to.equal(3) | ||
151 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(1) | ||
152 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1) | ||
153 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1) | ||
154 | } | ||
155 | |||
156 | { | ||
157 | await server.videos.upload({ attributes: { fixture: 'video_short.webm', channelId } }) | ||
158 | |||
159 | const data = await server.stats.get() | ||
160 | |||
161 | expect(data.totalLocalVideoChannels).to.equal(3) | ||
162 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(2) | ||
163 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(2) | ||
164 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(2) | ||
165 | } | ||
166 | }) | ||
167 | |||
168 | it('Should have the correct playlist stats', async function () { | ||
169 | const server = servers[0] | ||
170 | |||
171 | { | ||
172 | const data = await server.stats.get() | ||
173 | expect(data.totalLocalPlaylists).to.equal(0) | ||
174 | } | ||
175 | |||
176 | { | ||
177 | await server.playlists.create({ | ||
178 | attributes: { | ||
179 | displayName: 'playlist for count', | ||
180 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
181 | videoChannelId: channelId | ||
182 | } | ||
183 | }) | ||
184 | |||
185 | const data = await server.stats.get() | ||
186 | expect(data.totalLocalPlaylists).to.equal(1) | ||
187 | } | ||
188 | }) | ||
189 | |||
190 | it('Should correctly count video file sizes if transcoding is enabled', async function () { | ||
191 | this.timeout(120000) | ||
192 | |||
193 | await servers[0].config.updateCustomSubConfig({ | ||
194 | newConfig: { | ||
195 | transcoding: { | ||
196 | enabled: true, | ||
197 | webVideos: { | ||
198 | enabled: true | ||
199 | }, | ||
200 | hls: { | ||
201 | enabled: true | ||
202 | }, | ||
203 | resolutions: { | ||
204 | '0p': false, | ||
205 | '144p': false, | ||
206 | '240p': false, | ||
207 | '360p': false, | ||
208 | '480p': false, | ||
209 | '720p': false, | ||
210 | '1080p': false, | ||
211 | '1440p': false, | ||
212 | '2160p': false | ||
213 | } | ||
214 | } | ||
215 | } | ||
216 | }) | ||
217 | |||
218 | await servers[0].videos.upload({ attributes: { name: 'video', fixture: 'video_short.webm' } }) | ||
219 | |||
220 | await waitJobs(servers) | ||
221 | |||
222 | { | ||
223 | const data = await servers[1].stats.get() | ||
224 | expect(data.totalLocalVideoFilesSize).to.equal(0) | ||
225 | } | ||
226 | |||
227 | { | ||
228 | const data = await servers[0].stats.get() | ||
229 | expect(data.totalLocalVideoFilesSize).to.be.greaterThan(500000) | ||
230 | expect(data.totalLocalVideoFilesSize).to.be.lessThan(600000) | ||
231 | } | ||
232 | }) | ||
233 | |||
234 | it('Should have the correct AP stats', async function () { | ||
235 | this.timeout(120000) | ||
236 | |||
237 | await servers[0].config.disableTranscoding() | ||
238 | |||
239 | const first = await servers[1].stats.get() | ||
240 | |||
241 | for (let i = 0; i < 10; i++) { | ||
242 | await servers[0].videos.upload({ attributes: { name: 'video' } }) | ||
243 | } | ||
244 | |||
245 | await waitJobs(servers) | ||
246 | |||
247 | await wait(6000) | ||
248 | |||
249 | const second = await servers[1].stats.get() | ||
250 | expect(second.totalActivityPubMessagesProcessed).to.be.greaterThan(first.totalActivityPubMessagesProcessed) | ||
251 | |||
252 | const apTypes: ActivityType[] = [ | ||
253 | 'Create', 'Update', 'Delete', 'Follow', 'Accept', 'Announce', 'Undo', 'Like', 'Reject', 'View', 'Dislike', 'Flag' | ||
254 | ] | ||
255 | |||
256 | const processed = apTypes.reduce( | ||
257 | (previous, type) => previous + second['totalActivityPub' + type + 'MessagesSuccesses'], | ||
258 | 0 | ||
259 | ) | ||
260 | expect(second.totalActivityPubMessagesProcessed).to.equal(processed) | ||
261 | expect(second.totalActivityPubMessagesSuccesses).to.equal(processed) | ||
262 | |||
263 | expect(second.totalActivityPubMessagesErrors).to.equal(0) | ||
264 | |||
265 | for (const apType of apTypes) { | ||
266 | expect(second['totalActivityPub' + apType + 'MessagesErrors']).to.equal(0) | ||
267 | } | ||
268 | |||
269 | await wait(6000) | ||
270 | |||
271 | const third = await servers[1].stats.get() | ||
272 | expect(third.totalActivityPubMessagesWaiting).to.equal(0) | ||
273 | expect(third.activityPubMessagesProcessedPerSecond).to.be.lessThan(second.activityPubMessagesProcessedPerSecond) | ||
274 | }) | ||
275 | |||
276 | after(async function () { | ||
277 | await cleanupTests(servers) | ||
278 | }) | ||
279 | }) | ||
diff --git a/packages/tests/src/api/server/tracker.ts b/packages/tests/src/api/server/tracker.ts new file mode 100644 index 000000000..4df4e4613 --- /dev/null +++ b/packages/tests/src/api/server/tracker.ts | |||
@@ -0,0 +1,110 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await,@typescript-eslint/no-floating-promises */ | ||
2 | |||
3 | import { decode as magnetUriDecode, encode as magnetUriEncode } from 'magnet-uri' | ||
4 | import WebTorrent from 'webtorrent' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | killallServers, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test tracker', function () { | ||
14 | let server: PeerTubeServer | ||
15 | let badMagnet: string | ||
16 | let goodMagnet: string | ||
17 | |||
18 | before(async function () { | ||
19 | this.timeout(60000) | ||
20 | server = await createSingleServer(1) | ||
21 | await setAccessTokensToServers([ server ]) | ||
22 | |||
23 | { | ||
24 | const { uuid } = await server.videos.upload() | ||
25 | const video = await server.videos.get({ id: uuid }) | ||
26 | goodMagnet = video.files[0].magnetUri | ||
27 | |||
28 | const parsed = magnetUriDecode(goodMagnet) | ||
29 | parsed.infoHash = '010597bb88b1968a5693a4fa8267c592ca65f2e9' | ||
30 | |||
31 | badMagnet = magnetUriEncode(parsed) | ||
32 | } | ||
33 | }) | ||
34 | |||
35 | it('Should succeed with the correct infohash', function (done) { | ||
36 | const webtorrent = new WebTorrent() | ||
37 | |||
38 | const torrent = webtorrent.add(goodMagnet) | ||
39 | |||
40 | torrent.on('error', done) | ||
41 | torrent.on('warning', warn => { | ||
42 | const message = typeof warn === 'string' ? warn : warn.message | ||
43 | if (message.includes('Unknown infoHash ')) return done(new Error('Error on infohash')) | ||
44 | }) | ||
45 | |||
46 | torrent.on('done', done) | ||
47 | }) | ||
48 | |||
49 | it('Should disable the tracker', function (done) { | ||
50 | this.timeout(20000) | ||
51 | |||
52 | const errCb = () => done(new Error('Tracker is enabled')) | ||
53 | |||
54 | killallServers([ server ]) | ||
55 | .then(() => server.run({ tracker: { enabled: false } })) | ||
56 | .then(() => { | ||
57 | const webtorrent = new WebTorrent() | ||
58 | |||
59 | const torrent = webtorrent.add(goodMagnet) | ||
60 | |||
61 | torrent.on('error', done) | ||
62 | torrent.on('warning', warn => { | ||
63 | const message = typeof warn === 'string' ? warn : warn.message | ||
64 | if (message.includes('disabled ')) { | ||
65 | torrent.off('done', errCb) | ||
66 | |||
67 | return done() | ||
68 | } | ||
69 | }) | ||
70 | |||
71 | torrent.on('done', errCb) | ||
72 | }) | ||
73 | }) | ||
74 | |||
75 | it('Should return an error when adding an incorrect infohash', function (done) { | ||
76 | this.timeout(20000) | ||
77 | |||
78 | killallServers([ server ]) | ||
79 | .then(() => server.run()) | ||
80 | .then(() => { | ||
81 | const webtorrent = new WebTorrent() | ||
82 | |||
83 | const torrent = webtorrent.add(badMagnet) | ||
84 | |||
85 | torrent.on('error', done) | ||
86 | torrent.on('warning', warn => { | ||
87 | const message = typeof warn === 'string' ? warn : warn.message | ||
88 | if (message.includes('Unknown infoHash ')) return done() | ||
89 | }) | ||
90 | |||
91 | torrent.on('done', () => done(new Error('No error on infohash'))) | ||
92 | }) | ||
93 | }) | ||
94 | |||
95 | it('Should block the IP after the failed infohash', function (done) { | ||
96 | const webtorrent = new WebTorrent() | ||
97 | |||
98 | const torrent = webtorrent.add(goodMagnet) | ||
99 | |||
100 | torrent.on('error', done) | ||
101 | torrent.on('warning', warn => { | ||
102 | const message = typeof warn === 'string' ? warn : warn.message | ||
103 | if (message.includes('Unsupported tracker protocol')) return done() | ||
104 | }) | ||
105 | }) | ||
106 | |||
107 | after(async function () { | ||
108 | await cleanupTests([ server ]) | ||
109 | }) | ||
110 | }) | ||
diff --git a/packages/tests/src/api/transcoding/audio-only.ts b/packages/tests/src/api/transcoding/audio-only.ts new file mode 100644 index 000000000..6d0410348 --- /dev/null +++ b/packages/tests/src/api/transcoding/audio-only.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { getAudioStream, getVideoStreamDimensionsInfo } from '@peertube/peertube-ffmpeg' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test audio only video transcoding', function () { | ||
15 | let servers: PeerTubeServer[] = [] | ||
16 | let videoUUID: string | ||
17 | let webVideoAudioFileUrl: string | ||
18 | let fragmentedAudioFileUrl: string | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(120000) | ||
22 | |||
23 | const configOverride = { | ||
24 | transcoding: { | ||
25 | enabled: true, | ||
26 | resolutions: { | ||
27 | '0p': true, | ||
28 | '144p': false, | ||
29 | '240p': true, | ||
30 | '360p': false, | ||
31 | '480p': false, | ||
32 | '720p': false, | ||
33 | '1080p': false, | ||
34 | '1440p': false, | ||
35 | '2160p': false | ||
36 | }, | ||
37 | hls: { | ||
38 | enabled: true | ||
39 | }, | ||
40 | web_videos: { | ||
41 | enabled: true | ||
42 | } | ||
43 | } | ||
44 | } | ||
45 | servers = await createMultipleServers(2, configOverride) | ||
46 | |||
47 | // Get the access tokens | ||
48 | await setAccessTokensToServers(servers) | ||
49 | |||
50 | // Server 1 and server 2 follow each other | ||
51 | await doubleFollow(servers[0], servers[1]) | ||
52 | }) | ||
53 | |||
54 | it('Should upload a video and transcode it', async function () { | ||
55 | this.timeout(120000) | ||
56 | |||
57 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'audio only' } }) | ||
58 | videoUUID = uuid | ||
59 | |||
60 | await waitJobs(servers) | ||
61 | |||
62 | for (const server of servers) { | ||
63 | const video = await server.videos.get({ id: videoUUID }) | ||
64 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
65 | |||
66 | for (const files of [ video.files, video.streamingPlaylists[0].files ]) { | ||
67 | expect(files).to.have.lengthOf(3) | ||
68 | expect(files[0].resolution.id).to.equal(720) | ||
69 | expect(files[1].resolution.id).to.equal(240) | ||
70 | expect(files[2].resolution.id).to.equal(0) | ||
71 | } | ||
72 | |||
73 | if (server.serverNumber === 1) { | ||
74 | webVideoAudioFileUrl = video.files[2].fileUrl | ||
75 | fragmentedAudioFileUrl = video.streamingPlaylists[0].files[2].fileUrl | ||
76 | } | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | it('0p transcoded video should not have video', async function () { | ||
81 | const paths = [ | ||
82 | servers[0].servers.buildWebVideoFilePath(webVideoAudioFileUrl), | ||
83 | servers[0].servers.buildFragmentedFilePath(videoUUID, fragmentedAudioFileUrl) | ||
84 | ] | ||
85 | |||
86 | for (const path of paths) { | ||
87 | const { audioStream } = await getAudioStream(path) | ||
88 | expect(audioStream['codec_name']).to.be.equal('aac') | ||
89 | expect(audioStream['bit_rate']).to.be.at.most(384 * 8000) | ||
90 | |||
91 | const size = await getVideoStreamDimensionsInfo(path) | ||
92 | |||
93 | expect(size.height).to.equal(0) | ||
94 | expect(size.width).to.equal(0) | ||
95 | expect(size.isPortraitMode).to.be.false | ||
96 | expect(size.ratio).to.equal(0) | ||
97 | expect(size.resolution).to.equal(0) | ||
98 | } | ||
99 | }) | ||
100 | |||
101 | after(async function () { | ||
102 | await cleanupTests(servers) | ||
103 | }) | ||
104 | }) | ||
diff --git a/packages/tests/src/api/transcoding/create-transcoding.ts b/packages/tests/src/api/transcoding/create-transcoding.ts new file mode 100644 index 000000000..b0a9c7556 --- /dev/null +++ b/packages/tests/src/api/transcoding/create-transcoding.ts | |||
@@ -0,0 +1,267 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | expectNoFailedTranscodingJob, | ||
12 | makeRawRequest, | ||
13 | ObjectStorageCommand, | ||
14 | PeerTubeServer, | ||
15 | setAccessTokensToServers, | ||
16 | waitJobs | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | import { expectStartWith } from '@tests/shared/checks.js' | ||
19 | import { checkResolutionsInMasterPlaylist } from '@tests/shared/streaming-playlists.js' | ||
20 | |||
21 | async function checkFilesInObjectStorage (objectStorage: ObjectStorageCommand, video: VideoDetails) { | ||
22 | for (const file of video.files) { | ||
23 | expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
24 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
25 | } | ||
26 | |||
27 | if (video.streamingPlaylists.length === 0) return | ||
28 | |||
29 | const hlsPlaylist = video.streamingPlaylists[0] | ||
30 | for (const file of hlsPlaylist.files) { | ||
31 | expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
32 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
33 | } | ||
34 | |||
35 | expectStartWith(hlsPlaylist.playlistUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
36 | await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
37 | |||
38 | expectStartWith(hlsPlaylist.segmentsSha256Url, objectStorage.getMockPlaylistBaseUrl()) | ||
39 | await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
40 | } | ||
41 | |||
42 | function runTests (enableObjectStorage: boolean) { | ||
43 | let servers: PeerTubeServer[] = [] | ||
44 | let videoUUID: string | ||
45 | let publishedAt: string | ||
46 | |||
47 | let shouldBeDeleted: string[] | ||
48 | const objectStorage = new ObjectStorageCommand() | ||
49 | |||
50 | before(async function () { | ||
51 | this.timeout(120000) | ||
52 | |||
53 | const config = enableObjectStorage | ||
54 | ? objectStorage.getDefaultMockConfig() | ||
55 | : {} | ||
56 | |||
57 | // Run server 2 to have transcoding enabled | ||
58 | servers = await createMultipleServers(2, config) | ||
59 | await setAccessTokensToServers(servers) | ||
60 | |||
61 | await servers[0].config.disableTranscoding() | ||
62 | |||
63 | await doubleFollow(servers[0], servers[1]) | ||
64 | |||
65 | if (enableObjectStorage) await objectStorage.prepareDefaultMockBuckets() | ||
66 | |||
67 | const { shortUUID } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
68 | videoUUID = shortUUID | ||
69 | |||
70 | await waitJobs(servers) | ||
71 | |||
72 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
73 | publishedAt = video.publishedAt as string | ||
74 | |||
75 | await servers[0].config.enableTranscoding() | ||
76 | }) | ||
77 | |||
78 | it('Should generate HLS', async function () { | ||
79 | this.timeout(60000) | ||
80 | |||
81 | await servers[0].videos.runTranscoding({ | ||
82 | videoId: videoUUID, | ||
83 | transcodingType: 'hls' | ||
84 | }) | ||
85 | |||
86 | await waitJobs(servers) | ||
87 | await expectNoFailedTranscodingJob(servers[0]) | ||
88 | |||
89 | for (const server of servers) { | ||
90 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
91 | |||
92 | expect(videoDetails.files).to.have.lengthOf(1) | ||
93 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
94 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
95 | |||
96 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
97 | } | ||
98 | }) | ||
99 | |||
100 | it('Should generate Web Video', async function () { | ||
101 | this.timeout(60000) | ||
102 | |||
103 | await servers[0].videos.runTranscoding({ | ||
104 | videoId: videoUUID, | ||
105 | transcodingType: 'web-video' | ||
106 | }) | ||
107 | |||
108 | await waitJobs(servers) | ||
109 | |||
110 | for (const server of servers) { | ||
111 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
112 | |||
113 | expect(videoDetails.files).to.have.lengthOf(5) | ||
114 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
115 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
116 | |||
117 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
118 | } | ||
119 | }) | ||
120 | |||
121 | it('Should generate Web Video from HLS only video', async function () { | ||
122 | this.timeout(60000) | ||
123 | |||
124 | await servers[0].videos.removeAllWebVideoFiles({ videoId: videoUUID }) | ||
125 | await waitJobs(servers) | ||
126 | |||
127 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' }) | ||
128 | await waitJobs(servers) | ||
129 | |||
130 | for (const server of servers) { | ||
131 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
132 | |||
133 | expect(videoDetails.files).to.have.lengthOf(5) | ||
134 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
135 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
136 | |||
137 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
138 | } | ||
139 | }) | ||
140 | |||
141 | it('Should only generate Web Video', async function () { | ||
142 | this.timeout(60000) | ||
143 | |||
144 | await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID }) | ||
145 | await waitJobs(servers) | ||
146 | |||
147 | await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' }) | ||
148 | await waitJobs(servers) | ||
149 | |||
150 | for (const server of servers) { | ||
151 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
152 | |||
153 | expect(videoDetails.files).to.have.lengthOf(5) | ||
154 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(0) | ||
155 | |||
156 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
157 | } | ||
158 | }) | ||
159 | |||
160 | it('Should correctly update HLS playlist on resolution change', async function () { | ||
161 | this.timeout(120000) | ||
162 | |||
163 | await servers[0].config.updateExistingSubConfig({ | ||
164 | newConfig: { | ||
165 | transcoding: { | ||
166 | enabled: true, | ||
167 | resolutions: ConfigCommand.getCustomConfigResolutions(false), | ||
168 | |||
169 | webVideos: { | ||
170 | enabled: true | ||
171 | }, | ||
172 | hls: { | ||
173 | enabled: true | ||
174 | } | ||
175 | } | ||
176 | } | ||
177 | }) | ||
178 | |||
179 | const { uuid } = await servers[0].videos.quickUpload({ name: 'quick' }) | ||
180 | |||
181 | await waitJobs(servers) | ||
182 | |||
183 | for (const server of servers) { | ||
184 | const videoDetails = await server.videos.get({ id: uuid }) | ||
185 | |||
186 | expect(videoDetails.files).to.have.lengthOf(1) | ||
187 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
188 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(1) | ||
189 | |||
190 | if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
191 | |||
192 | shouldBeDeleted = [ | ||
193 | videoDetails.streamingPlaylists[0].files[0].fileUrl, | ||
194 | videoDetails.streamingPlaylists[0].playlistUrl, | ||
195 | videoDetails.streamingPlaylists[0].segmentsSha256Url | ||
196 | ] | ||
197 | } | ||
198 | |||
199 | await servers[0].config.updateExistingSubConfig({ | ||
200 | newConfig: { | ||
201 | transcoding: { | ||
202 | enabled: true, | ||
203 | resolutions: ConfigCommand.getCustomConfigResolutions(true), | ||
204 | |||
205 | webVideos: { | ||
206 | enabled: true | ||
207 | }, | ||
208 | hls: { | ||
209 | enabled: true | ||
210 | } | ||
211 | } | ||
212 | } | ||
213 | }) | ||
214 | |||
215 | await servers[0].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' }) | ||
216 | await waitJobs(servers) | ||
217 | |||
218 | for (const server of servers) { | ||
219 | const videoDetails = await server.videos.get({ id: uuid }) | ||
220 | |||
221 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
222 | expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5) | ||
223 | |||
224 | if (enableObjectStorage) { | ||
225 | await checkFilesInObjectStorage(objectStorage, videoDetails) | ||
226 | |||
227 | const hlsPlaylist = videoDetails.streamingPlaylists[0] | ||
228 | const resolutions = hlsPlaylist.files.map(f => f.resolution.id) | ||
229 | await checkResolutionsInMasterPlaylist({ server: servers[0], playlistUrl: hlsPlaylist.playlistUrl, resolutions }) | ||
230 | |||
231 | const shaBody = await servers[0].streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: true }) | ||
232 | expect(Object.keys(shaBody)).to.have.lengthOf(5) | ||
233 | } | ||
234 | } | ||
235 | }) | ||
236 | |||
237 | it('Should have correctly deleted previous files', async function () { | ||
238 | for (const fileUrl of shouldBeDeleted) { | ||
239 | await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
240 | } | ||
241 | }) | ||
242 | |||
243 | it('Should not have updated published at attributes', async function () { | ||
244 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
245 | |||
246 | expect(video.publishedAt).to.equal(publishedAt) | ||
247 | }) | ||
248 | |||
249 | after(async function () { | ||
250 | if (objectStorage) await objectStorage.cleanupMock() | ||
251 | |||
252 | await cleanupTests(servers) | ||
253 | }) | ||
254 | } | ||
255 | |||
256 | describe('Test create transcoding jobs from API', function () { | ||
257 | |||
258 | describe('On filesystem', function () { | ||
259 | runTests(false) | ||
260 | }) | ||
261 | |||
262 | describe('On object storage', function () { | ||
263 | if (areMockObjectStorageTestsDisabled()) return | ||
264 | |||
265 | runTests(true) | ||
266 | }) | ||
267 | }) | ||
diff --git a/packages/tests/src/api/transcoding/hls.ts b/packages/tests/src/api/transcoding/hls.ts new file mode 100644 index 000000000..884f98e87 --- /dev/null +++ b/packages/tests/src/api/transcoding/hls.ts | |||
@@ -0,0 +1,176 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { join } from 'path' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { DEFAULT_AUDIO_RESOLUTION } from '@peertube/peertube-server/server/initializers/constants.js' | ||
16 | import { checkDirectoryIsEmpty, checkTmpIsEmpty } from '@tests/shared/directories.js' | ||
17 | import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' | ||
18 | |||
19 | describe('Test HLS videos', function () { | ||
20 | let servers: PeerTubeServer[] = [] | ||
21 | |||
22 | function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) { | ||
23 | const videoUUIDs: string[] = [] | ||
24 | |||
25 | it('Should upload a video and transcode it to HLS', async function () { | ||
26 | this.timeout(120000) | ||
27 | |||
28 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1', fixture: 'video_short.webm' } }) | ||
29 | videoUUIDs.push(uuid) | ||
30 | |||
31 | await waitJobs(servers) | ||
32 | |||
33 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
34 | }) | ||
35 | |||
36 | it('Should upload an audio file and transcode it to HLS', async function () { | ||
37 | this.timeout(120000) | ||
38 | |||
39 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video audio', fixture: 'sample.ogg' } }) | ||
40 | videoUUIDs.push(uuid) | ||
41 | |||
42 | await waitJobs(servers) | ||
43 | |||
44 | await completeCheckHlsPlaylist({ | ||
45 | servers, | ||
46 | videoUUID: uuid, | ||
47 | hlsOnly, | ||
48 | resolutions: [ DEFAULT_AUDIO_RESOLUTION, 360, 240 ], | ||
49 | objectStorageBaseUrl | ||
50 | }) | ||
51 | }) | ||
52 | |||
53 | it('Should update the video', async function () { | ||
54 | this.timeout(30000) | ||
55 | |||
56 | await servers[0].videos.update({ id: videoUUIDs[0], attributes: { name: 'video 1 updated' } }) | ||
57 | |||
58 | await waitJobs(servers) | ||
59 | |||
60 | await completeCheckHlsPlaylist({ servers, videoUUID: videoUUIDs[0], hlsOnly, objectStorageBaseUrl }) | ||
61 | }) | ||
62 | |||
63 | it('Should delete videos', async function () { | ||
64 | for (const uuid of videoUUIDs) { | ||
65 | await servers[0].videos.remove({ id: uuid }) | ||
66 | } | ||
67 | |||
68 | await waitJobs(servers) | ||
69 | |||
70 | for (const server of servers) { | ||
71 | for (const uuid of videoUUIDs) { | ||
72 | await server.videos.get({ id: uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
73 | } | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | it('Should have the playlists/segment deleted from the disk', async function () { | ||
78 | for (const server of servers) { | ||
79 | await checkDirectoryIsEmpty(server, 'web-videos', [ 'private' ]) | ||
80 | await checkDirectoryIsEmpty(server, join('web-videos', 'private')) | ||
81 | |||
82 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'), [ 'private' ]) | ||
83 | await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls', 'private')) | ||
84 | } | ||
85 | }) | ||
86 | |||
87 | it('Should have an empty tmp directory', async function () { | ||
88 | for (const server of servers) { | ||
89 | await checkTmpIsEmpty(server) | ||
90 | } | ||
91 | }) | ||
92 | } | ||
93 | |||
94 | before(async function () { | ||
95 | this.timeout(120000) | ||
96 | |||
97 | const configOverride = { | ||
98 | transcoding: { | ||
99 | enabled: true, | ||
100 | allow_audio_files: true, | ||
101 | hls: { | ||
102 | enabled: true | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | servers = await createMultipleServers(2, configOverride) | ||
107 | |||
108 | // Get the access tokens | ||
109 | await setAccessTokensToServers(servers) | ||
110 | |||
111 | // Server 1 and server 2 follow each other | ||
112 | await doubleFollow(servers[0], servers[1]) | ||
113 | }) | ||
114 | |||
115 | describe('With Web Video & HLS enabled', function () { | ||
116 | runTestSuite(false) | ||
117 | }) | ||
118 | |||
119 | describe('With only HLS enabled', function () { | ||
120 | |||
121 | before(async function () { | ||
122 | await servers[0].config.updateCustomSubConfig({ | ||
123 | newConfig: { | ||
124 | transcoding: { | ||
125 | enabled: true, | ||
126 | allowAudioFiles: true, | ||
127 | resolutions: { | ||
128 | '144p': false, | ||
129 | '240p': true, | ||
130 | '360p': true, | ||
131 | '480p': true, | ||
132 | '720p': true, | ||
133 | '1080p': true, | ||
134 | '1440p': true, | ||
135 | '2160p': true | ||
136 | }, | ||
137 | hls: { | ||
138 | enabled: true | ||
139 | }, | ||
140 | webVideos: { | ||
141 | enabled: false | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | }) | ||
146 | }) | ||
147 | |||
148 | runTestSuite(true) | ||
149 | }) | ||
150 | |||
151 | describe('With object storage enabled', function () { | ||
152 | if (areMockObjectStorageTestsDisabled()) return | ||
153 | |||
154 | const objectStorage = new ObjectStorageCommand() | ||
155 | |||
156 | before(async function () { | ||
157 | this.timeout(120000) | ||
158 | |||
159 | const configOverride = objectStorage.getDefaultMockConfig() | ||
160 | await objectStorage.prepareDefaultMockBuckets() | ||
161 | |||
162 | await servers[0].kill() | ||
163 | await servers[0].run(configOverride) | ||
164 | }) | ||
165 | |||
166 | runTestSuite(true, objectStorage.getMockPlaylistBaseUrl()) | ||
167 | |||
168 | after(async function () { | ||
169 | await objectStorage.cleanupMock() | ||
170 | }) | ||
171 | }) | ||
172 | |||
173 | after(async function () { | ||
174 | await cleanupTests(servers) | ||
175 | }) | ||
176 | }) | ||
diff --git a/packages/tests/src/api/transcoding/index.ts b/packages/tests/src/api/transcoding/index.ts new file mode 100644 index 000000000..c25cd51c3 --- /dev/null +++ b/packages/tests/src/api/transcoding/index.ts | |||
@@ -0,0 +1,6 @@ | |||
1 | export * from './audio-only.js' | ||
2 | export * from './create-transcoding.js' | ||
3 | export * from './hls.js' | ||
4 | export * from './transcoder.js' | ||
5 | export * from './update-while-transcoding.js' | ||
6 | export * from './video-studio.js' | ||
diff --git a/packages/tests/src/api/transcoding/transcoder.ts b/packages/tests/src/api/transcoding/transcoder.ts new file mode 100644 index 000000000..8900491f5 --- /dev/null +++ b/packages/tests/src/api/transcoding/transcoder.ts | |||
@@ -0,0 +1,802 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { getAllFiles, getMaxTheoreticalBitrate, getMinTheoreticalBitrate, omit } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, VideoFileMetadata, VideoState } from '@peertube/peertube-models' | ||
6 | import { canDoQuickTranscode } from '@peertube/peertube-server/server/lib/transcoding/transcoding-quick-transcode.js' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | ffprobePromise, | ||
10 | getAudioStream, | ||
11 | getVideoStreamBitrate, | ||
12 | getVideoStreamDimensionsInfo, | ||
13 | getVideoStreamFPS, | ||
14 | hasAudioStream | ||
15 | } from '@peertube/peertube-ffmpeg' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | makeGetRequest, | ||
21 | PeerTubeServer, | ||
22 | setAccessTokensToServers, | ||
23 | waitJobs | ||
24 | } from '@peertube/peertube-server-commands' | ||
25 | import { generateVideoWithFramerate, generateHighBitrateVideo } from '@tests/shared/generate.js' | ||
26 | import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js' | ||
27 | |||
28 | function updateConfigForTranscoding (server: PeerTubeServer) { | ||
29 | return server.config.updateCustomSubConfig({ | ||
30 | newConfig: { | ||
31 | transcoding: { | ||
32 | enabled: true, | ||
33 | allowAdditionalExtensions: true, | ||
34 | allowAudioFiles: true, | ||
35 | hls: { enabled: true }, | ||
36 | webVideos: { enabled: true }, | ||
37 | resolutions: { | ||
38 | '0p': false, | ||
39 | '144p': true, | ||
40 | '240p': true, | ||
41 | '360p': true, | ||
42 | '480p': true, | ||
43 | '720p': true, | ||
44 | '1080p': true, | ||
45 | '1440p': true, | ||
46 | '2160p': true | ||
47 | } | ||
48 | } | ||
49 | } | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | describe('Test video transcoding', function () { | ||
54 | let servers: PeerTubeServer[] = [] | ||
55 | let video4k: string | ||
56 | |||
57 | before(async function () { | ||
58 | this.timeout(30_000) | ||
59 | |||
60 | // Run servers | ||
61 | servers = await createMultipleServers(2) | ||
62 | |||
63 | await setAccessTokensToServers(servers) | ||
64 | |||
65 | await doubleFollow(servers[0], servers[1]) | ||
66 | |||
67 | await updateConfigForTranscoding(servers[1]) | ||
68 | }) | ||
69 | |||
70 | describe('Basic transcoding (or not)', function () { | ||
71 | |||
72 | it('Should not transcode video on server 1', async function () { | ||
73 | this.timeout(60_000) | ||
74 | |||
75 | const attributes = { | ||
76 | name: 'my super name for server 1', | ||
77 | description: 'my super description for server 1', | ||
78 | fixture: 'video_short.webm' | ||
79 | } | ||
80 | await servers[0].videos.upload({ attributes }) | ||
81 | |||
82 | await waitJobs(servers) | ||
83 | |||
84 | for (const server of servers) { | ||
85 | const { data } = await server.videos.list() | ||
86 | const video = data[0] | ||
87 | |||
88 | const videoDetails = await server.videos.get({ id: video.id }) | ||
89 | expect(videoDetails.files).to.have.lengthOf(1) | ||
90 | |||
91 | const magnetUri = videoDetails.files[0].magnetUri | ||
92 | expect(magnetUri).to.match(/\.webm/) | ||
93 | |||
94 | await checkWebTorrentWorks(magnetUri, /\.webm$/) | ||
95 | } | ||
96 | }) | ||
97 | |||
98 | it('Should transcode video on server 2', async function () { | ||
99 | this.timeout(120_000) | ||
100 | |||
101 | const attributes = { | ||
102 | name: 'my super name for server 2', | ||
103 | description: 'my super description for server 2', | ||
104 | fixture: 'video_short.webm' | ||
105 | } | ||
106 | await servers[1].videos.upload({ attributes }) | ||
107 | |||
108 | await waitJobs(servers) | ||
109 | |||
110 | for (const server of servers) { | ||
111 | const { data } = await server.videos.list() | ||
112 | |||
113 | const video = data.find(v => v.name === attributes.name) | ||
114 | const videoDetails = await server.videos.get({ id: video.id }) | ||
115 | |||
116 | expect(videoDetails.files).to.have.lengthOf(5) | ||
117 | |||
118 | const magnetUri = videoDetails.files[0].magnetUri | ||
119 | expect(magnetUri).to.match(/\.mp4/) | ||
120 | |||
121 | await checkWebTorrentWorks(magnetUri, /\.mp4$/) | ||
122 | } | ||
123 | }) | ||
124 | |||
125 | it('Should wait for transcoding before publishing the video', async function () { | ||
126 | this.timeout(160_000) | ||
127 | |||
128 | { | ||
129 | // Upload the video, but wait transcoding | ||
130 | const attributes = { | ||
131 | name: 'waiting video', | ||
132 | fixture: 'video_short1.webm', | ||
133 | waitTranscoding: true | ||
134 | } | ||
135 | const { uuid } = await servers[1].videos.upload({ attributes }) | ||
136 | const videoId = uuid | ||
137 | |||
138 | // Should be in transcode state | ||
139 | const body = await servers[1].videos.get({ id: videoId }) | ||
140 | expect(body.name).to.equal('waiting video') | ||
141 | expect(body.state.id).to.equal(VideoState.TO_TRANSCODE) | ||
142 | expect(body.state.label).to.equal('To transcode') | ||
143 | expect(body.waitTranscoding).to.be.true | ||
144 | |||
145 | { | ||
146 | // Should have my video | ||
147 | const { data } = await servers[1].videos.listMyVideos() | ||
148 | const videoToFindInMine = data.find(v => v.name === attributes.name) | ||
149 | expect(videoToFindInMine).not.to.be.undefined | ||
150 | expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE) | ||
151 | expect(videoToFindInMine.state.label).to.equal('To transcode') | ||
152 | expect(videoToFindInMine.waitTranscoding).to.be.true | ||
153 | } | ||
154 | |||
155 | { | ||
156 | // Should not list this video | ||
157 | const { data } = await servers[1].videos.list() | ||
158 | const videoToFindInList = data.find(v => v.name === attributes.name) | ||
159 | expect(videoToFindInList).to.be.undefined | ||
160 | } | ||
161 | |||
162 | // Server 1 should not have the video yet | ||
163 | await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
164 | } | ||
165 | |||
166 | await waitJobs(servers) | ||
167 | |||
168 | for (const server of servers) { | ||
169 | const { data } = await server.videos.list() | ||
170 | const videoToFind = data.find(v => v.name === 'waiting video') | ||
171 | expect(videoToFind).not.to.be.undefined | ||
172 | |||
173 | const videoDetails = await server.videos.get({ id: videoToFind.id }) | ||
174 | |||
175 | expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED) | ||
176 | expect(videoDetails.state.label).to.equal('Published') | ||
177 | expect(videoDetails.waitTranscoding).to.be.true | ||
178 | } | ||
179 | }) | ||
180 | |||
181 | it('Should accept and transcode additional extensions', async function () { | ||
182 | this.timeout(300_000) | ||
183 | |||
184 | for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { | ||
185 | const attributes = { | ||
186 | name: fixture, | ||
187 | fixture | ||
188 | } | ||
189 | |||
190 | await servers[1].videos.upload({ attributes }) | ||
191 | |||
192 | await waitJobs(servers) | ||
193 | |||
194 | for (const server of servers) { | ||
195 | const { data } = await server.videos.list() | ||
196 | |||
197 | const video = data.find(v => v.name === attributes.name) | ||
198 | const videoDetails = await server.videos.get({ id: video.id }) | ||
199 | expect(videoDetails.files).to.have.lengthOf(5) | ||
200 | |||
201 | const magnetUri = videoDetails.files[0].magnetUri | ||
202 | expect(magnetUri).to.contain('.mp4') | ||
203 | } | ||
204 | } | ||
205 | }) | ||
206 | |||
207 | it('Should transcode a 4k video', async function () { | ||
208 | this.timeout(200_000) | ||
209 | |||
210 | const attributes = { | ||
211 | name: '4k video', | ||
212 | fixture: 'video_short_4k.mp4' | ||
213 | } | ||
214 | |||
215 | const { uuid } = await servers[1].videos.upload({ attributes }) | ||
216 | video4k = uuid | ||
217 | |||
218 | await waitJobs(servers) | ||
219 | |||
220 | const resolutions = [ 144, 240, 360, 480, 720, 1080, 1440, 2160 ] | ||
221 | |||
222 | for (const server of servers) { | ||
223 | const videoDetails = await server.videos.get({ id: video4k }) | ||
224 | expect(videoDetails.files).to.have.lengthOf(resolutions.length) | ||
225 | |||
226 | for (const r of resolutions) { | ||
227 | expect(videoDetails.files.find(f => f.resolution.id === r)).to.not.be.undefined | ||
228 | expect(videoDetails.streamingPlaylists[0].files.find(f => f.resolution.id === r)).to.not.be.undefined | ||
229 | } | ||
230 | } | ||
231 | }) | ||
232 | }) | ||
233 | |||
234 | describe('Audio transcoding', function () { | ||
235 | |||
236 | it('Should transcode high bit rate mp3 to proper bit rate', async function () { | ||
237 | this.timeout(60_000) | ||
238 | |||
239 | const attributes = { | ||
240 | name: 'mp3_256k', | ||
241 | fixture: 'video_short_mp3_256k.mp4' | ||
242 | } | ||
243 | await servers[1].videos.upload({ attributes }) | ||
244 | |||
245 | await waitJobs(servers) | ||
246 | |||
247 | for (const server of servers) { | ||
248 | const { data } = await server.videos.list() | ||
249 | |||
250 | const video = data.find(v => v.name === attributes.name) | ||
251 | const videoDetails = await server.videos.get({ id: video.id }) | ||
252 | |||
253 | expect(videoDetails.files).to.have.lengthOf(5) | ||
254 | |||
255 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
256 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
257 | const probe = await getAudioStream(path) | ||
258 | |||
259 | if (probe.audioStream) { | ||
260 | expect(probe.audioStream['codec_name']).to.be.equal('aac') | ||
261 | expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000) | ||
262 | } else { | ||
263 | this.fail('Could not retrieve the audio stream on ' + probe.absolutePath) | ||
264 | } | ||
265 | } | ||
266 | }) | ||
267 | |||
268 | it('Should transcode video with no audio and have no audio itself', async function () { | ||
269 | this.timeout(60_000) | ||
270 | |||
271 | const attributes = { | ||
272 | name: 'no_audio', | ||
273 | fixture: 'video_short_no_audio.mp4' | ||
274 | } | ||
275 | await servers[1].videos.upload({ attributes }) | ||
276 | |||
277 | await waitJobs(servers) | ||
278 | |||
279 | for (const server of servers) { | ||
280 | const { data } = await server.videos.list() | ||
281 | |||
282 | const video = data.find(v => v.name === attributes.name) | ||
283 | const videoDetails = await server.videos.get({ id: video.id }) | ||
284 | |||
285 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
286 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
287 | |||
288 | expect(await hasAudioStream(path)).to.be.false | ||
289 | } | ||
290 | }) | ||
291 | |||
292 | it('Should leave the audio untouched, but properly transcode the video', async function () { | ||
293 | this.timeout(60_000) | ||
294 | |||
295 | const attributes = { | ||
296 | name: 'untouched_audio', | ||
297 | fixture: 'video_short.mp4' | ||
298 | } | ||
299 | await servers[1].videos.upload({ attributes }) | ||
300 | |||
301 | await waitJobs(servers) | ||
302 | |||
303 | for (const server of servers) { | ||
304 | const { data } = await server.videos.list() | ||
305 | |||
306 | const video = data.find(v => v.name === attributes.name) | ||
307 | const videoDetails = await server.videos.get({ id: video.id }) | ||
308 | |||
309 | expect(videoDetails.files).to.have.lengthOf(5) | ||
310 | |||
311 | const fixturePath = buildAbsoluteFixturePath(attributes.fixture) | ||
312 | const fixtureVideoProbe = await getAudioStream(fixturePath) | ||
313 | |||
314 | const file = videoDetails.files.find(f => f.resolution.id === 240) | ||
315 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
316 | |||
317 | const videoProbe = await getAudioStream(path) | ||
318 | |||
319 | if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { | ||
320 | const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] | ||
321 | expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) | ||
322 | } else { | ||
323 | this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath) | ||
324 | } | ||
325 | } | ||
326 | }) | ||
327 | }) | ||
328 | |||
329 | describe('Audio upload', function () { | ||
330 | |||
331 | function runSuite (mode: 'legacy' | 'resumable') { | ||
332 | |||
333 | before(async function () { | ||
334 | await servers[1].config.updateCustomSubConfig({ | ||
335 | newConfig: { | ||
336 | transcoding: { | ||
337 | hls: { enabled: true }, | ||
338 | webVideos: { enabled: true }, | ||
339 | resolutions: { | ||
340 | '0p': false, | ||
341 | '144p': false, | ||
342 | '240p': false, | ||
343 | '360p': false, | ||
344 | '480p': false, | ||
345 | '720p': false, | ||
346 | '1080p': false, | ||
347 | '1440p': false, | ||
348 | '2160p': false | ||
349 | } | ||
350 | } | ||
351 | } | ||
352 | }) | ||
353 | }) | ||
354 | |||
355 | it('Should merge an audio file with the preview file', async function () { | ||
356 | this.timeout(60_000) | ||
357 | |||
358 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } | ||
359 | await servers[1].videos.upload({ attributes, mode }) | ||
360 | |||
361 | await waitJobs(servers) | ||
362 | |||
363 | for (const server of servers) { | ||
364 | const { data } = await server.videos.list() | ||
365 | |||
366 | const video = data.find(v => v.name === 'audio_with_preview') | ||
367 | const videoDetails = await server.videos.get({ id: video.id }) | ||
368 | |||
369 | expect(videoDetails.files).to.have.lengthOf(1) | ||
370 | |||
371 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
372 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
373 | |||
374 | const magnetUri = videoDetails.files[0].magnetUri | ||
375 | expect(magnetUri).to.contain('.mp4') | ||
376 | } | ||
377 | }) | ||
378 | |||
379 | it('Should upload an audio file and choose a default background image', async function () { | ||
380 | this.timeout(60_000) | ||
381 | |||
382 | const attributes = { name: 'audio_without_preview', fixture: 'sample.ogg' } | ||
383 | await servers[1].videos.upload({ attributes, mode }) | ||
384 | |||
385 | await waitJobs(servers) | ||
386 | |||
387 | for (const server of servers) { | ||
388 | const { data } = await server.videos.list() | ||
389 | |||
390 | const video = data.find(v => v.name === 'audio_without_preview') | ||
391 | const videoDetails = await server.videos.get({ id: video.id }) | ||
392 | |||
393 | expect(videoDetails.files).to.have.lengthOf(1) | ||
394 | |||
395 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
396 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
397 | |||
398 | const magnetUri = videoDetails.files[0].magnetUri | ||
399 | expect(magnetUri).to.contain('.mp4') | ||
400 | } | ||
401 | }) | ||
402 | |||
403 | it('Should upload an audio file and create an audio version only', async function () { | ||
404 | this.timeout(60_000) | ||
405 | |||
406 | await servers[1].config.updateCustomSubConfig({ | ||
407 | newConfig: { | ||
408 | transcoding: { | ||
409 | hls: { enabled: true }, | ||
410 | webVideos: { enabled: true }, | ||
411 | resolutions: { | ||
412 | '0p': true, | ||
413 | '144p': false, | ||
414 | '240p': false, | ||
415 | '360p': false | ||
416 | } | ||
417 | } | ||
418 | } | ||
419 | }) | ||
420 | |||
421 | const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } | ||
422 | const { id } = await servers[1].videos.upload({ attributes, mode }) | ||
423 | |||
424 | await waitJobs(servers) | ||
425 | |||
426 | for (const server of servers) { | ||
427 | const videoDetails = await server.videos.get({ id }) | ||
428 | |||
429 | for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) { | ||
430 | expect(files).to.have.lengthOf(2) | ||
431 | expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined | ||
432 | } | ||
433 | } | ||
434 | |||
435 | await updateConfigForTranscoding(servers[1]) | ||
436 | }) | ||
437 | } | ||
438 | |||
439 | describe('Legacy upload', function () { | ||
440 | runSuite('legacy') | ||
441 | }) | ||
442 | |||
443 | describe('Resumable upload', function () { | ||
444 | runSuite('resumable') | ||
445 | }) | ||
446 | }) | ||
447 | |||
448 | describe('Framerate', function () { | ||
449 | |||
450 | it('Should transcode a 60 FPS video', async function () { | ||
451 | this.timeout(60_000) | ||
452 | |||
453 | const attributes = { | ||
454 | name: 'my super 30fps name for server 2', | ||
455 | description: 'my super 30fps description for server 2', | ||
456 | fixture: '60fps_720p_small.mp4' | ||
457 | } | ||
458 | await servers[1].videos.upload({ attributes }) | ||
459 | |||
460 | await waitJobs(servers) | ||
461 | |||
462 | for (const server of servers) { | ||
463 | const { data } = await server.videos.list() | ||
464 | |||
465 | const video = data.find(v => v.name === attributes.name) | ||
466 | const videoDetails = await server.videos.get({ id: video.id }) | ||
467 | |||
468 | expect(videoDetails.files).to.have.lengthOf(5) | ||
469 | expect(videoDetails.files[0].fps).to.be.above(58).and.below(62) | ||
470 | expect(videoDetails.files[1].fps).to.be.below(31) | ||
471 | expect(videoDetails.files[2].fps).to.be.below(31) | ||
472 | expect(videoDetails.files[3].fps).to.be.below(31) | ||
473 | expect(videoDetails.files[4].fps).to.be.below(31) | ||
474 | |||
475 | for (const resolution of [ 144, 240, 360, 480 ]) { | ||
476 | const file = videoDetails.files.find(f => f.resolution.id === resolution) | ||
477 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
478 | const fps = await getVideoStreamFPS(path) | ||
479 | |||
480 | expect(fps).to.be.below(31) | ||
481 | } | ||
482 | |||
483 | const file = videoDetails.files.find(f => f.resolution.id === 720) | ||
484 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
485 | const fps = await getVideoStreamFPS(path) | ||
486 | |||
487 | expect(fps).to.be.above(58).and.below(62) | ||
488 | } | ||
489 | }) | ||
490 | |||
491 | it('Should downscale to the closest divisor standard framerate', async function () { | ||
492 | this.timeout(200_000) | ||
493 | |||
494 | let tempFixturePath: string | ||
495 | |||
496 | { | ||
497 | tempFixturePath = await generateVideoWithFramerate(59) | ||
498 | |||
499 | const fps = await getVideoStreamFPS(tempFixturePath) | ||
500 | expect(fps).to.be.equal(59) | ||
501 | } | ||
502 | |||
503 | const attributes = { | ||
504 | name: '59fps video', | ||
505 | description: '59fps video', | ||
506 | fixture: tempFixturePath | ||
507 | } | ||
508 | |||
509 | await servers[1].videos.upload({ attributes }) | ||
510 | |||
511 | await waitJobs(servers) | ||
512 | |||
513 | for (const server of servers) { | ||
514 | const { data } = await server.videos.list() | ||
515 | |||
516 | const { id } = data.find(v => v.name === attributes.name) | ||
517 | const video = await server.videos.get({ id }) | ||
518 | |||
519 | { | ||
520 | const file = video.files.find(f => f.resolution.id === 240) | ||
521 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
522 | const fps = await getVideoStreamFPS(path) | ||
523 | expect(fps).to.be.equal(25) | ||
524 | } | ||
525 | |||
526 | { | ||
527 | const file = video.files.find(f => f.resolution.id === 720) | ||
528 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
529 | const fps = await getVideoStreamFPS(path) | ||
530 | expect(fps).to.be.equal(59) | ||
531 | } | ||
532 | } | ||
533 | }) | ||
534 | }) | ||
535 | |||
536 | describe('Bitrate control', function () { | ||
537 | |||
538 | it('Should respect maximum bitrate values', async function () { | ||
539 | this.timeout(160_000) | ||
540 | |||
541 | const tempFixturePath = await generateHighBitrateVideo() | ||
542 | |||
543 | const attributes = { | ||
544 | name: 'high bitrate video', | ||
545 | description: 'high bitrate video', | ||
546 | fixture: tempFixturePath | ||
547 | } | ||
548 | |||
549 | await servers[1].videos.upload({ attributes }) | ||
550 | |||
551 | await waitJobs(servers) | ||
552 | |||
553 | for (const server of servers) { | ||
554 | const { data } = await server.videos.list() | ||
555 | |||
556 | const { id } = data.find(v => v.name === attributes.name) | ||
557 | const video = await server.videos.get({ id }) | ||
558 | |||
559 | for (const resolution of [ 240, 360, 480, 720, 1080 ]) { | ||
560 | const file = video.files.find(f => f.resolution.id === resolution) | ||
561 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
562 | |||
563 | const bitrate = await getVideoStreamBitrate(path) | ||
564 | const fps = await getVideoStreamFPS(path) | ||
565 | const dataResolution = await getVideoStreamDimensionsInfo(path) | ||
566 | |||
567 | expect(resolution).to.equal(resolution) | ||
568 | |||
569 | const maxBitrate = getMaxTheoreticalBitrate({ ...dataResolution, fps }) | ||
570 | expect(bitrate).to.be.below(maxBitrate) | ||
571 | } | ||
572 | } | ||
573 | }) | ||
574 | |||
575 | it('Should not transcode to an higher bitrate than the original file but above our low limit', async function () { | ||
576 | this.timeout(160_000) | ||
577 | |||
578 | const newConfig = { | ||
579 | transcoding: { | ||
580 | enabled: true, | ||
581 | resolutions: { | ||
582 | '144p': true, | ||
583 | '240p': true, | ||
584 | '360p': true, | ||
585 | '480p': true, | ||
586 | '720p': true, | ||
587 | '1080p': true, | ||
588 | '1440p': true, | ||
589 | '2160p': true | ||
590 | }, | ||
591 | webVideos: { enabled: true }, | ||
592 | hls: { enabled: true } | ||
593 | } | ||
594 | } | ||
595 | await servers[1].config.updateCustomSubConfig({ newConfig }) | ||
596 | |||
597 | const attributes = { | ||
598 | name: 'low bitrate', | ||
599 | fixture: 'low-bitrate.mp4' | ||
600 | } | ||
601 | |||
602 | const { id } = await servers[1].videos.upload({ attributes }) | ||
603 | |||
604 | await waitJobs(servers) | ||
605 | |||
606 | const video = await servers[1].videos.get({ id }) | ||
607 | |||
608 | const resolutions = [ 240, 360, 480, 720, 1080 ] | ||
609 | for (const r of resolutions) { | ||
610 | const file = video.files.find(f => f.resolution.id === r) | ||
611 | |||
612 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
613 | const bitrate = await getVideoStreamBitrate(path) | ||
614 | |||
615 | const inputBitrate = 60_000 | ||
616 | const limit = getMinTheoreticalBitrate({ fps: 10, ratio: 1, resolution: r }) | ||
617 | let belowValue = Math.max(inputBitrate, limit) | ||
618 | belowValue += belowValue * 0.20 // Apply 20% margin because bitrate control is not very precise | ||
619 | |||
620 | expect(bitrate, `${path} not below ${limit}`).to.be.below(belowValue) | ||
621 | } | ||
622 | }) | ||
623 | }) | ||
624 | |||
625 | describe('FFprobe', function () { | ||
626 | |||
627 | it('Should provide valid ffprobe data', async function () { | ||
628 | this.timeout(160_000) | ||
629 | |||
630 | const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid | ||
631 | await waitJobs(servers) | ||
632 | |||
633 | { | ||
634 | const video = await servers[1].videos.get({ id: videoUUID }) | ||
635 | const file = video.files.find(f => f.resolution.id === 240) | ||
636 | const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl) | ||
637 | |||
638 | const probe = await ffprobePromise(path) | ||
639 | const metadata = new VideoFileMetadata(probe) | ||
640 | |||
641 | // expected format properties | ||
642 | for (const p of [ | ||
643 | 'tags.encoder', | ||
644 | 'format_long_name', | ||
645 | 'size', | ||
646 | 'bit_rate' | ||
647 | ]) { | ||
648 | expect(metadata.format).to.have.nested.property(p) | ||
649 | } | ||
650 | |||
651 | // expected stream properties | ||
652 | for (const p of [ | ||
653 | 'codec_long_name', | ||
654 | 'profile', | ||
655 | 'width', | ||
656 | 'height', | ||
657 | 'display_aspect_ratio', | ||
658 | 'avg_frame_rate', | ||
659 | 'pix_fmt' | ||
660 | ]) { | ||
661 | expect(metadata.streams[0]).to.have.nested.property(p) | ||
662 | } | ||
663 | |||
664 | expect(metadata).to.not.have.nested.property('format.filename') | ||
665 | } | ||
666 | |||
667 | for (const server of servers) { | ||
668 | const videoDetails = await server.videos.get({ id: videoUUID }) | ||
669 | |||
670 | const videoFiles = getAllFiles(videoDetails) | ||
671 | expect(videoFiles).to.have.lengthOf(10) | ||
672 | |||
673 | for (const file of videoFiles) { | ||
674 | expect(file.metadata).to.be.undefined | ||
675 | expect(file.metadataUrl).to.exist | ||
676 | expect(file.metadataUrl).to.contain(servers[1].url) | ||
677 | expect(file.metadataUrl).to.contain(videoUUID) | ||
678 | |||
679 | const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl }) | ||
680 | expect(metadata).to.have.nested.property('format.size') | ||
681 | } | ||
682 | } | ||
683 | }) | ||
684 | |||
685 | it('Should correctly detect if quick transcode is possible', async function () { | ||
686 | this.timeout(10_000) | ||
687 | |||
688 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true | ||
689 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false | ||
690 | }) | ||
691 | }) | ||
692 | |||
693 | describe('Transcoding job queue', function () { | ||
694 | |||
695 | it('Should have the appropriate priorities for transcoding jobs', async function () { | ||
696 | const body = await servers[1].jobs.list({ | ||
697 | start: 0, | ||
698 | count: 100, | ||
699 | sort: 'createdAt', | ||
700 | jobType: 'video-transcoding' | ||
701 | }) | ||
702 | |||
703 | const jobs = body.data | ||
704 | const transcodingJobs = jobs.filter(j => j.data.videoUUID === video4k) | ||
705 | |||
706 | expect(transcodingJobs).to.have.lengthOf(16) | ||
707 | |||
708 | const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls') | ||
709 | const webVideoJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-web-video') | ||
710 | const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-web-video') | ||
711 | |||
712 | expect(hlsJobs).to.have.lengthOf(8) | ||
713 | expect(webVideoJobs).to.have.lengthOf(7) | ||
714 | expect(optimizeJobs).to.have.lengthOf(1) | ||
715 | |||
716 | for (const j of optimizeJobs.concat(hlsJobs.concat(webVideoJobs))) { | ||
717 | expect(j.priority).to.be.greaterThan(100) | ||
718 | expect(j.priority).to.be.lessThan(150) | ||
719 | } | ||
720 | }) | ||
721 | }) | ||
722 | |||
723 | describe('Bounded transcoding', function () { | ||
724 | |||
725 | it('Should not generate an upper resolution than original file', async function () { | ||
726 | this.timeout(120_000) | ||
727 | |||
728 | await servers[0].config.updateExistingSubConfig({ | ||
729 | newConfig: { | ||
730 | transcoding: { | ||
731 | enabled: true, | ||
732 | hls: { enabled: true }, | ||
733 | webVideos: { enabled: true }, | ||
734 | resolutions: { | ||
735 | '0p': false, | ||
736 | '144p': false, | ||
737 | '240p': true, | ||
738 | '360p': false, | ||
739 | '480p': true, | ||
740 | '720p': false, | ||
741 | '1080p': false, | ||
742 | '1440p': false, | ||
743 | '2160p': false | ||
744 | }, | ||
745 | alwaysTranscodeOriginalResolution: false | ||
746 | } | ||
747 | } | ||
748 | }) | ||
749 | |||
750 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' }) | ||
751 | await waitJobs(servers) | ||
752 | |||
753 | const video = await servers[0].videos.get({ id: uuid }) | ||
754 | const hlsFiles = video.streamingPlaylists[0].files | ||
755 | |||
756 | expect(video.files).to.have.lengthOf(2) | ||
757 | expect(hlsFiles).to.have.lengthOf(2) | ||
758 | |||
759 | // eslint-disable-next-line @typescript-eslint/require-array-sort-compare | ||
760 | const resolutions = getAllFiles(video).map(f => f.resolution.id).sort() | ||
761 | expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ]) | ||
762 | }) | ||
763 | |||
764 | it('Should only keep the original resolution if all resolutions are disabled', async function () { | ||
765 | this.timeout(120_000) | ||
766 | |||
767 | await servers[0].config.updateExistingSubConfig({ | ||
768 | newConfig: { | ||
769 | transcoding: { | ||
770 | resolutions: { | ||
771 | '0p': false, | ||
772 | '144p': false, | ||
773 | '240p': false, | ||
774 | '360p': false, | ||
775 | '480p': false, | ||
776 | '720p': false, | ||
777 | '1080p': false, | ||
778 | '1440p': false, | ||
779 | '2160p': false | ||
780 | } | ||
781 | } | ||
782 | } | ||
783 | }) | ||
784 | |||
785 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' }) | ||
786 | await waitJobs(servers) | ||
787 | |||
788 | const video = await servers[0].videos.get({ id: uuid }) | ||
789 | const hlsFiles = video.streamingPlaylists[0].files | ||
790 | |||
791 | expect(video.files).to.have.lengthOf(1) | ||
792 | expect(hlsFiles).to.have.lengthOf(1) | ||
793 | |||
794 | expect(video.files[0].resolution.id).to.equal(720) | ||
795 | expect(hlsFiles[0].resolution.id).to.equal(720) | ||
796 | }) | ||
797 | }) | ||
798 | |||
799 | after(async function () { | ||
800 | await cleanupTests(servers) | ||
801 | }) | ||
802 | }) | ||
diff --git a/packages/tests/src/api/transcoding/update-while-transcoding.ts b/packages/tests/src/api/transcoding/update-while-transcoding.ts new file mode 100644 index 000000000..9990bc745 --- /dev/null +++ b/packages/tests/src/api/transcoding/update-while-transcoding.ts | |||
@@ -0,0 +1,161 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' | ||
16 | |||
17 | describe('Test update video privacy while transcoding', function () { | ||
18 | let servers: PeerTubeServer[] = [] | ||
19 | |||
20 | const videoUUIDs: string[] = [] | ||
21 | |||
22 | function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) { | ||
23 | |||
24 | it('Should not have an error while quickly updating a private video to public after upload #1', async function () { | ||
25 | this.timeout(360_000) | ||
26 | |||
27 | const attributes = { | ||
28 | name: 'quick update', | ||
29 | privacy: VideoPrivacy.PRIVATE | ||
30 | } | ||
31 | |||
32 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: false }) | ||
33 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
34 | videoUUIDs.push(uuid) | ||
35 | |||
36 | await waitJobs(servers) | ||
37 | |||
38 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
39 | }) | ||
40 | |||
41 | it('Should not have an error while quickly updating a private video to public after upload #2', async function () { | ||
42 | this.timeout(60000) | ||
43 | |||
44 | { | ||
45 | const attributes = { | ||
46 | name: 'quick update 2', | ||
47 | privacy: VideoPrivacy.PRIVATE | ||
48 | } | ||
49 | |||
50 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: true }) | ||
51 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
52 | videoUUIDs.push(uuid) | ||
53 | |||
54 | await waitJobs(servers) | ||
55 | |||
56 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
57 | } | ||
58 | }) | ||
59 | |||
60 | it('Should not have an error while quickly updating a private video to public after upload #3', async function () { | ||
61 | this.timeout(60000) | ||
62 | |||
63 | const attributes = { | ||
64 | name: 'quick update 3', | ||
65 | privacy: VideoPrivacy.PRIVATE | ||
66 | } | ||
67 | |||
68 | const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: true }) | ||
69 | await wait(1000) | ||
70 | await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
71 | videoUUIDs.push(uuid) | ||
72 | |||
73 | await waitJobs(servers) | ||
74 | |||
75 | await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl }) | ||
76 | }) | ||
77 | } | ||
78 | |||
79 | before(async function () { | ||
80 | this.timeout(120000) | ||
81 | |||
82 | const configOverride = { | ||
83 | transcoding: { | ||
84 | enabled: true, | ||
85 | allow_audio_files: true, | ||
86 | hls: { | ||
87 | enabled: true | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | servers = await createMultipleServers(2, configOverride) | ||
92 | |||
93 | // Get the access tokens | ||
94 | await setAccessTokensToServers(servers) | ||
95 | |||
96 | // Server 1 and server 2 follow each other | ||
97 | await doubleFollow(servers[0], servers[1]) | ||
98 | }) | ||
99 | |||
100 | describe('With Web Video & HLS enabled', function () { | ||
101 | runTestSuite(false) | ||
102 | }) | ||
103 | |||
104 | describe('With only HLS enabled', function () { | ||
105 | |||
106 | before(async function () { | ||
107 | await servers[0].config.updateCustomSubConfig({ | ||
108 | newConfig: { | ||
109 | transcoding: { | ||
110 | enabled: true, | ||
111 | allowAudioFiles: true, | ||
112 | resolutions: { | ||
113 | '144p': false, | ||
114 | '240p': true, | ||
115 | '360p': true, | ||
116 | '480p': true, | ||
117 | '720p': true, | ||
118 | '1080p': true, | ||
119 | '1440p': true, | ||
120 | '2160p': true | ||
121 | }, | ||
122 | hls: { | ||
123 | enabled: true | ||
124 | }, | ||
125 | webVideos: { | ||
126 | enabled: false | ||
127 | } | ||
128 | } | ||
129 | } | ||
130 | }) | ||
131 | }) | ||
132 | |||
133 | runTestSuite(true) | ||
134 | }) | ||
135 | |||
136 | describe('With object storage enabled', function () { | ||
137 | if (areMockObjectStorageTestsDisabled()) return | ||
138 | |||
139 | const objectStorage = new ObjectStorageCommand() | ||
140 | |||
141 | before(async function () { | ||
142 | this.timeout(120000) | ||
143 | |||
144 | const configOverride = objectStorage.getDefaultMockConfig() | ||
145 | await objectStorage.prepareDefaultMockBuckets() | ||
146 | |||
147 | await servers[0].kill() | ||
148 | await servers[0].run(configOverride) | ||
149 | }) | ||
150 | |||
151 | runTestSuite(true, objectStorage.getMockPlaylistBaseUrl()) | ||
152 | |||
153 | after(async function () { | ||
154 | await objectStorage.cleanupMock() | ||
155 | }) | ||
156 | }) | ||
157 | |||
158 | after(async function () { | ||
159 | await cleanupTests(servers) | ||
160 | }) | ||
161 | }) | ||
diff --git a/packages/tests/src/api/transcoding/video-studio.ts b/packages/tests/src/api/transcoding/video-studio.ts new file mode 100644 index 000000000..8a3788aa6 --- /dev/null +++ b/packages/tests/src/api/transcoding/video-studio.ts | |||
@@ -0,0 +1,379 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { getAllFiles } from '@peertube/peertube-core-utils' | ||
3 | import { VideoStudioTask } from '@peertube/peertube-models' | ||
4 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | ObjectStorageCommand, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | VideoStudioCommand, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | import { checkVideoDuration, expectStartWith } from '@tests/shared/checks.js' | ||
17 | import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js' | ||
18 | |||
19 | describe('Test video studio', function () { | ||
20 | let servers: PeerTubeServer[] = [] | ||
21 | let videoUUID: string | ||
22 | |||
23 | async function renewVideo (fixture = 'video_short.webm') { | ||
24 | const video = await servers[0].videos.quickUpload({ name: 'video', fixture }) | ||
25 | videoUUID = video.uuid | ||
26 | |||
27 | await waitJobs(servers) | ||
28 | } | ||
29 | |||
30 | async function createTasks (tasks: VideoStudioTask[]) { | ||
31 | await servers[0].videoStudio.createEditionTasks({ videoId: videoUUID, tasks }) | ||
32 | await waitJobs(servers) | ||
33 | } | ||
34 | |||
35 | before(async function () { | ||
36 | this.timeout(120_000) | ||
37 | |||
38 | servers = await createMultipleServers(2) | ||
39 | |||
40 | await setAccessTokensToServers(servers) | ||
41 | await setDefaultVideoChannel(servers) | ||
42 | |||
43 | await doubleFollow(servers[0], servers[1]) | ||
44 | |||
45 | await servers[0].config.enableMinimumTranscoding() | ||
46 | |||
47 | await servers[0].config.enableStudio() | ||
48 | }) | ||
49 | |||
50 | describe('Cutting', function () { | ||
51 | |||
52 | it('Should cut the beginning of the video', async function () { | ||
53 | this.timeout(120_000) | ||
54 | |||
55 | await renewVideo() | ||
56 | await waitJobs(servers) | ||
57 | |||
58 | const beforeTasks = new Date() | ||
59 | |||
60 | await createTasks([ | ||
61 | { | ||
62 | name: 'cut', | ||
63 | options: { | ||
64 | start: 2 | ||
65 | } | ||
66 | } | ||
67 | ]) | ||
68 | |||
69 | for (const server of servers) { | ||
70 | await checkVideoDuration(server, videoUUID, 3) | ||
71 | |||
72 | const video = await server.videos.get({ id: videoUUID }) | ||
73 | expect(new Date(video.publishedAt)).to.be.below(beforeTasks) | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | it('Should cut the end of the video', async function () { | ||
78 | this.timeout(120_000) | ||
79 | await renewVideo() | ||
80 | |||
81 | await createTasks([ | ||
82 | { | ||
83 | name: 'cut', | ||
84 | options: { | ||
85 | end: 2 | ||
86 | } | ||
87 | } | ||
88 | ]) | ||
89 | |||
90 | for (const server of servers) { | ||
91 | await checkVideoDuration(server, videoUUID, 2) | ||
92 | } | ||
93 | }) | ||
94 | |||
95 | it('Should cut start/end of the video', async function () { | ||
96 | this.timeout(120_000) | ||
97 | await renewVideo('video_short1.webm') // 10 seconds video duration | ||
98 | |||
99 | await createTasks([ | ||
100 | { | ||
101 | name: 'cut', | ||
102 | options: { | ||
103 | start: 2, | ||
104 | end: 6 | ||
105 | } | ||
106 | } | ||
107 | ]) | ||
108 | |||
109 | for (const server of servers) { | ||
110 | await checkVideoDuration(server, videoUUID, 4) | ||
111 | } | ||
112 | }) | ||
113 | }) | ||
114 | |||
115 | describe('Intro/Outro', function () { | ||
116 | |||
117 | it('Should add an intro', async function () { | ||
118 | this.timeout(120_000) | ||
119 | await renewVideo() | ||
120 | |||
121 | await createTasks([ | ||
122 | { | ||
123 | name: 'add-intro', | ||
124 | options: { | ||
125 | file: 'video_short.webm' | ||
126 | } | ||
127 | } | ||
128 | ]) | ||
129 | |||
130 | for (const server of servers) { | ||
131 | await checkVideoDuration(server, videoUUID, 10) | ||
132 | } | ||
133 | }) | ||
134 | |||
135 | it('Should add an outro', async function () { | ||
136 | this.timeout(120_000) | ||
137 | await renewVideo() | ||
138 | |||
139 | await createTasks([ | ||
140 | { | ||
141 | name: 'add-outro', | ||
142 | options: { | ||
143 | file: 'video_very_short_240p.mp4' | ||
144 | } | ||
145 | } | ||
146 | ]) | ||
147 | |||
148 | for (const server of servers) { | ||
149 | await checkVideoDuration(server, videoUUID, 7) | ||
150 | } | ||
151 | }) | ||
152 | |||
153 | it('Should add an intro/outro', async function () { | ||
154 | this.timeout(120_000) | ||
155 | await renewVideo() | ||
156 | |||
157 | await createTasks([ | ||
158 | { | ||
159 | name: 'add-intro', | ||
160 | options: { | ||
161 | file: 'video_very_short_240p.mp4' | ||
162 | } | ||
163 | }, | ||
164 | { | ||
165 | name: 'add-outro', | ||
166 | options: { | ||
167 | // Different frame rate | ||
168 | file: 'video_short2.webm' | ||
169 | } | ||
170 | } | ||
171 | ]) | ||
172 | |||
173 | for (const server of servers) { | ||
174 | await checkVideoDuration(server, videoUUID, 12) | ||
175 | } | ||
176 | }) | ||
177 | |||
178 | it('Should add an intro to a video without audio', async function () { | ||
179 | this.timeout(120_000) | ||
180 | await renewVideo('video_short_no_audio.mp4') | ||
181 | |||
182 | await createTasks([ | ||
183 | { | ||
184 | name: 'add-intro', | ||
185 | options: { | ||
186 | file: 'video_very_short_240p.mp4' | ||
187 | } | ||
188 | } | ||
189 | ]) | ||
190 | |||
191 | for (const server of servers) { | ||
192 | await checkVideoDuration(server, videoUUID, 7) | ||
193 | } | ||
194 | }) | ||
195 | |||
196 | it('Should add an outro without audio to a video with audio', async function () { | ||
197 | this.timeout(120_000) | ||
198 | await renewVideo() | ||
199 | |||
200 | await createTasks([ | ||
201 | { | ||
202 | name: 'add-outro', | ||
203 | options: { | ||
204 | file: 'video_short_no_audio.mp4' | ||
205 | } | ||
206 | } | ||
207 | ]) | ||
208 | |||
209 | for (const server of servers) { | ||
210 | await checkVideoDuration(server, videoUUID, 10) | ||
211 | } | ||
212 | }) | ||
213 | |||
214 | it('Should add an outro without audio to a video with audio', async function () { | ||
215 | this.timeout(120_000) | ||
216 | await renewVideo('video_short_no_audio.mp4') | ||
217 | |||
218 | await createTasks([ | ||
219 | { | ||
220 | name: 'add-outro', | ||
221 | options: { | ||
222 | file: 'video_short_no_audio.mp4' | ||
223 | } | ||
224 | } | ||
225 | ]) | ||
226 | |||
227 | for (const server of servers) { | ||
228 | await checkVideoDuration(server, videoUUID, 10) | ||
229 | } | ||
230 | }) | ||
231 | }) | ||
232 | |||
233 | describe('Watermark', function () { | ||
234 | |||
235 | it('Should add a watermark to the video', async function () { | ||
236 | this.timeout(120_000) | ||
237 | await renewVideo() | ||
238 | |||
239 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
240 | const oldFileUrls = getAllFiles(video).map(f => f.fileUrl) | ||
241 | |||
242 | await createTasks([ | ||
243 | { | ||
244 | name: 'add-watermark', | ||
245 | options: { | ||
246 | file: 'custom-thumbnail.png' | ||
247 | } | ||
248 | } | ||
249 | ]) | ||
250 | |||
251 | for (const server of servers) { | ||
252 | const video = await server.videos.get({ id: videoUUID }) | ||
253 | const fileUrls = getAllFiles(video).map(f => f.fileUrl) | ||
254 | |||
255 | for (const oldUrl of oldFileUrls) { | ||
256 | expect(fileUrls).to.not.include(oldUrl) | ||
257 | } | ||
258 | } | ||
259 | }) | ||
260 | }) | ||
261 | |||
262 | describe('Complex tasks', function () { | ||
263 | it('Should run a complex task', async function () { | ||
264 | this.timeout(240_000) | ||
265 | await renewVideo() | ||
266 | |||
267 | await createTasks(VideoStudioCommand.getComplexTask()) | ||
268 | |||
269 | for (const server of servers) { | ||
270 | await checkVideoDuration(server, videoUUID, 9) | ||
271 | } | ||
272 | }) | ||
273 | }) | ||
274 | |||
275 | describe('HLS only studio edition', function () { | ||
276 | |||
277 | before(async function () { | ||
278 | // Disable Web Videos | ||
279 | await servers[0].config.updateExistingSubConfig({ | ||
280 | newConfig: { | ||
281 | transcoding: { | ||
282 | webVideos: { | ||
283 | enabled: false | ||
284 | } | ||
285 | } | ||
286 | } | ||
287 | }) | ||
288 | }) | ||
289 | |||
290 | it('Should run a complex task on HLS only video', async function () { | ||
291 | this.timeout(240_000) | ||
292 | await renewVideo() | ||
293 | |||
294 | await createTasks(VideoStudioCommand.getComplexTask()) | ||
295 | |||
296 | for (const server of servers) { | ||
297 | const video = await server.videos.get({ id: videoUUID }) | ||
298 | expect(video.files).to.have.lengthOf(0) | ||
299 | |||
300 | await checkVideoDuration(server, videoUUID, 9) | ||
301 | } | ||
302 | }) | ||
303 | }) | ||
304 | |||
305 | describe('Server restart', function () { | ||
306 | |||
307 | it('Should still be able to run video edition after a server restart', async function () { | ||
308 | this.timeout(240_000) | ||
309 | |||
310 | await renewVideo() | ||
311 | await servers[0].videoStudio.createEditionTasks({ videoId: videoUUID, tasks: VideoStudioCommand.getComplexTask() }) | ||
312 | |||
313 | await servers[0].kill() | ||
314 | await servers[0].run() | ||
315 | |||
316 | await waitJobs(servers) | ||
317 | |||
318 | for (const server of servers) { | ||
319 | await checkVideoDuration(server, videoUUID, 9) | ||
320 | } | ||
321 | }) | ||
322 | |||
323 | it('Should have an empty persistent tmp directory', async function () { | ||
324 | await checkPersistentTmpIsEmpty(servers[0]) | ||
325 | }) | ||
326 | }) | ||
327 | |||
328 | describe('Object storage studio edition', function () { | ||
329 | if (areMockObjectStorageTestsDisabled()) return | ||
330 | |||
331 | const objectStorage = new ObjectStorageCommand() | ||
332 | |||
333 | before(async function () { | ||
334 | await objectStorage.prepareDefaultMockBuckets() | ||
335 | |||
336 | await servers[0].kill() | ||
337 | await servers[0].run(objectStorage.getDefaultMockConfig()) | ||
338 | |||
339 | await servers[0].config.enableMinimumTranscoding() | ||
340 | }) | ||
341 | |||
342 | it('Should run a complex task on a video in object storage', async function () { | ||
343 | this.timeout(240_000) | ||
344 | await renewVideo() | ||
345 | |||
346 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
347 | const oldFileUrls = getAllFiles(video).map(f => f.fileUrl) | ||
348 | |||
349 | await createTasks(VideoStudioCommand.getComplexTask()) | ||
350 | |||
351 | for (const server of servers) { | ||
352 | const video = await server.videos.get({ id: videoUUID }) | ||
353 | const files = getAllFiles(video) | ||
354 | |||
355 | for (const f of files) { | ||
356 | expect(oldFileUrls).to.not.include(f.fileUrl) | ||
357 | } | ||
358 | |||
359 | for (const webVideoFile of video.files) { | ||
360 | expectStartWith(webVideoFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
361 | } | ||
362 | |||
363 | for (const hlsFile of video.streamingPlaylists[0].files) { | ||
364 | expectStartWith(hlsFile.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
365 | } | ||
366 | |||
367 | await checkVideoDuration(server, videoUUID, 9) | ||
368 | } | ||
369 | }) | ||
370 | |||
371 | after(async function () { | ||
372 | await objectStorage.cleanupMock() | ||
373 | }) | ||
374 | }) | ||
375 | |||
376 | after(async function () { | ||
377 | await cleanupTests(servers) | ||
378 | }) | ||
379 | }) | ||
diff --git a/packages/tests/src/api/users/index.ts b/packages/tests/src/api/users/index.ts new file mode 100644 index 000000000..830d4da62 --- /dev/null +++ b/packages/tests/src/api/users/index.ts | |||
@@ -0,0 +1,8 @@ | |||
1 | import './oauth.js' | ||
2 | import './registrations`.js' | ||
3 | import './two-factor.js' | ||
4 | import './user-subscriptions.js' | ||
5 | import './user-videos.js' | ||
6 | import './users.js' | ||
7 | import './users-multiple-servers.js' | ||
8 | import './users-email-verification.js' | ||
diff --git a/packages/tests/src/api/users/oauth.ts b/packages/tests/src/api/users/oauth.ts new file mode 100644 index 000000000..fe50872cb --- /dev/null +++ b/packages/tests/src/api/users/oauth.ts | |||
@@ -0,0 +1,203 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, OAuth2ErrorCode, PeerTubeProblemDocument } from '@peertube/peertube-models' | ||
6 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test oauth', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let sqlCommand: SQLCommand | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(30000) | ||
21 | |||
22 | server = await createSingleServer(1, { | ||
23 | rates_limit: { | ||
24 | login: { | ||
25 | max: 30 | ||
26 | } | ||
27 | } | ||
28 | }) | ||
29 | |||
30 | await setAccessTokensToServers([ server ]) | ||
31 | |||
32 | sqlCommand = new SQLCommand(server) | ||
33 | }) | ||
34 | |||
35 | describe('OAuth client', function () { | ||
36 | |||
37 | function expectInvalidClient (body: PeerTubeProblemDocument) { | ||
38 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
39 | expect(body.error).to.contain('client is invalid') | ||
40 | expect(body.type.startsWith('https://')).to.be.true | ||
41 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
42 | } | ||
43 | |||
44 | it('Should create a new client') | ||
45 | |||
46 | it('Should return the first client') | ||
47 | |||
48 | it('Should remove the last client') | ||
49 | |||
50 | it('Should not login with an invalid client id', async function () { | ||
51 | const client = { id: 'client', secret: server.store.client.secret } | ||
52 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
53 | |||
54 | expectInvalidClient(body) | ||
55 | }) | ||
56 | |||
57 | it('Should not login with an invalid client secret', async function () { | ||
58 | const client = { id: server.store.client.id, secret: 'coucou' } | ||
59 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
60 | |||
61 | expectInvalidClient(body) | ||
62 | }) | ||
63 | }) | ||
64 | |||
65 | describe('Login', function () { | ||
66 | |||
67 | function expectInvalidCredentials (body: PeerTubeProblemDocument) { | ||
68 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
69 | expect(body.error).to.contain('credentials are invalid') | ||
70 | expect(body.type.startsWith('https://')).to.be.true | ||
71 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
72 | } | ||
73 | |||
74 | it('Should not login with an invalid username', async function () { | ||
75 | const user = { username: 'captain crochet', password: server.store.user.password } | ||
76 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
77 | |||
78 | expectInvalidCredentials(body) | ||
79 | }) | ||
80 | |||
81 | it('Should not login with an invalid password', async function () { | ||
82 | const user = { username: server.store.user.username, password: 'mew_three' } | ||
83 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
84 | |||
85 | expectInvalidCredentials(body) | ||
86 | }) | ||
87 | |||
88 | it('Should be able to login', async function () { | ||
89 | await server.login.login({ expectedStatus: HttpStatusCode.OK_200 }) | ||
90 | }) | ||
91 | |||
92 | it('Should be able to login with an insensitive username', async function () { | ||
93 | const user = { username: 'RoOt', password: server.store.user.password } | ||
94 | await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) | ||
95 | |||
96 | const user2 = { username: 'rOoT', password: server.store.user.password } | ||
97 | await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 }) | ||
98 | |||
99 | const user3 = { username: 'ROOt', password: server.store.user.password } | ||
100 | await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 }) | ||
101 | }) | ||
102 | }) | ||
103 | |||
104 | describe('Logout', function () { | ||
105 | |||
106 | it('Should logout (revoke token)', async function () { | ||
107 | await server.login.logout({ token: server.accessToken }) | ||
108 | }) | ||
109 | |||
110 | it('Should not be able to get the user information', async function () { | ||
111 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
112 | }) | ||
113 | |||
114 | it('Should not be able to upload a video', async function () { | ||
115 | await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
116 | }) | ||
117 | |||
118 | it('Should be able to login again', async function () { | ||
119 | const body = await server.login.login() | ||
120 | server.accessToken = body.access_token | ||
121 | server.refreshToken = body.refresh_token | ||
122 | }) | ||
123 | |||
124 | it('Should be able to get my user information again', async function () { | ||
125 | await server.users.getMyInfo() | ||
126 | }) | ||
127 | |||
128 | it('Should have an expired access token', async function () { | ||
129 | this.timeout(60000) | ||
130 | |||
131 | await sqlCommand.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
132 | await sqlCommand.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
133 | |||
134 | await killallServers([ server ]) | ||
135 | await server.run() | ||
136 | |||
137 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
138 | }) | ||
139 | |||
140 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
141 | await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
142 | }) | ||
143 | |||
144 | it('Should refresh the token', async function () { | ||
145 | this.timeout(50000) | ||
146 | |||
147 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
148 | await sqlCommand.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
149 | |||
150 | await killallServers([ server ]) | ||
151 | await server.run() | ||
152 | |||
153 | const res = await server.login.refreshToken({ refreshToken: server.refreshToken }) | ||
154 | server.accessToken = res.body.access_token | ||
155 | server.refreshToken = res.body.refresh_token | ||
156 | }) | ||
157 | |||
158 | it('Should be able to get my user information again', async function () { | ||
159 | await server.users.getMyInfo() | ||
160 | }) | ||
161 | }) | ||
162 | |||
163 | describe('Custom token lifetime', function () { | ||
164 | before(async function () { | ||
165 | this.timeout(120_000) | ||
166 | |||
167 | await server.kill() | ||
168 | await server.run({ | ||
169 | oauth2: { | ||
170 | token_lifetime: { | ||
171 | access_token: '2 seconds', | ||
172 | refresh_token: '2 seconds' | ||
173 | } | ||
174 | } | ||
175 | }) | ||
176 | }) | ||
177 | |||
178 | it('Should have a very short access token lifetime', async function () { | ||
179 | this.timeout(50000) | ||
180 | |||
181 | const { access_token: accessToken } = await server.login.login() | ||
182 | await server.users.getMyInfo({ token: accessToken }) | ||
183 | |||
184 | await wait(3000) | ||
185 | await server.users.getMyInfo({ token: accessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
186 | }) | ||
187 | |||
188 | it('Should have a very short refresh token lifetime', async function () { | ||
189 | this.timeout(50000) | ||
190 | |||
191 | const { refresh_token: refreshToken } = await server.login.login() | ||
192 | await server.login.refreshToken({ refreshToken }) | ||
193 | |||
194 | await wait(3000) | ||
195 | await server.login.refreshToken({ refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
196 | }) | ||
197 | }) | ||
198 | |||
199 | after(async function () { | ||
200 | await sqlCommand.cleanup() | ||
201 | await cleanupTests([ server ]) | ||
202 | }) | ||
203 | }) | ||
diff --git a/packages/tests/src/api/users/registrations.ts b/packages/tests/src/api/users/registrations.ts new file mode 100644 index 000000000..dbe1bc4f5 --- /dev/null +++ b/packages/tests/src/api/users/registrations.ts | |||
@@ -0,0 +1,415 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { UserRegistrationState, UserRole } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test registrations', function () { | ||
16 | let server: PeerTubeServer | ||
17 | |||
18 | const emails: object[] = [] | ||
19 | let emailPort: number | ||
20 | |||
21 | before(async function () { | ||
22 | this.timeout(30000) | ||
23 | |||
24 | emailPort = await MockSmtpServer.Instance.collectEmails(emails) | ||
25 | |||
26 | server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(emailPort)) | ||
27 | |||
28 | await setAccessTokensToServers([ server ]) | ||
29 | await server.config.enableSignup(false) | ||
30 | }) | ||
31 | |||
32 | describe('Direct registrations of a new user', function () { | ||
33 | let user1Token: string | ||
34 | |||
35 | it('Should register a new user', async function () { | ||
36 | const user = { displayName: 'super user 1', username: 'user_1', password: 'my super password' } | ||
37 | const channel = { name: 'my_user_1_channel', displayName: 'my channel rocks' } | ||
38 | |||
39 | await server.registrations.register({ ...user, channel }) | ||
40 | }) | ||
41 | |||
42 | it('Should be able to login with this registered user', async function () { | ||
43 | const user1 = { username: 'user_1', password: 'my super password' } | ||
44 | |||
45 | user1Token = await server.login.getAccessToken(user1) | ||
46 | }) | ||
47 | |||
48 | it('Should have the correct display name', async function () { | ||
49 | const user = await server.users.getMyInfo({ token: user1Token }) | ||
50 | expect(user.account.displayName).to.equal('super user 1') | ||
51 | }) | ||
52 | |||
53 | it('Should have the correct video quota', async function () { | ||
54 | const user = await server.users.getMyInfo({ token: user1Token }) | ||
55 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) | ||
56 | }) | ||
57 | |||
58 | it('Should have created the channel', async function () { | ||
59 | const { displayName } = await server.channels.get({ channelName: 'my_user_1_channel' }) | ||
60 | |||
61 | expect(displayName).to.equal('my channel rocks') | ||
62 | }) | ||
63 | |||
64 | it('Should remove me', async function () { | ||
65 | { | ||
66 | const { data } = await server.users.list() | ||
67 | expect(data.find(u => u.username === 'user_1')).to.not.be.undefined | ||
68 | } | ||
69 | |||
70 | await server.users.deleteMe({ token: user1Token }) | ||
71 | |||
72 | { | ||
73 | const { data } = await server.users.list() | ||
74 | expect(data.find(u => u.username === 'user_1')).to.be.undefined | ||
75 | } | ||
76 | }) | ||
77 | }) | ||
78 | |||
79 | describe('Registration requests', function () { | ||
80 | let id2: number | ||
81 | let id3: number | ||
82 | let id4: number | ||
83 | |||
84 | let user2Token: string | ||
85 | let user3Token: string | ||
86 | |||
87 | before(async function () { | ||
88 | this.timeout(60000) | ||
89 | |||
90 | await server.config.enableSignup(true) | ||
91 | |||
92 | { | ||
93 | const { id } = await server.registrations.requestRegistration({ | ||
94 | username: 'user4', | ||
95 | registrationReason: 'registration reason 4' | ||
96 | }) | ||
97 | |||
98 | id4 = id | ||
99 | } | ||
100 | }) | ||
101 | |||
102 | it('Should request a registration without a channel', async function () { | ||
103 | { | ||
104 | const { id } = await server.registrations.requestRegistration({ | ||
105 | username: 'user2', | ||
106 | displayName: 'my super user 2', | ||
107 | email: 'user2@example.com', | ||
108 | password: 'user2password', | ||
109 | registrationReason: 'registration reason 2' | ||
110 | }) | ||
111 | |||
112 | id2 = id | ||
113 | } | ||
114 | }) | ||
115 | |||
116 | it('Should request a registration with a channel', async function () { | ||
117 | const { id } = await server.registrations.requestRegistration({ | ||
118 | username: 'user3', | ||
119 | displayName: 'my super user 3', | ||
120 | channel: { | ||
121 | displayName: 'my user 3 channel', | ||
122 | name: 'super_user3_channel' | ||
123 | }, | ||
124 | email: 'user3@example.com', | ||
125 | password: 'user3password', | ||
126 | registrationReason: 'registration reason 3' | ||
127 | }) | ||
128 | |||
129 | id3 = id | ||
130 | }) | ||
131 | |||
132 | it('Should list these registration requests', async function () { | ||
133 | { | ||
134 | const { total, data } = await server.registrations.list({ sort: '-createdAt' }) | ||
135 | expect(total).to.equal(3) | ||
136 | expect(data).to.have.lengthOf(3) | ||
137 | |||
138 | { | ||
139 | expect(data[0].id).to.equal(id3) | ||
140 | expect(data[0].username).to.equal('user3') | ||
141 | expect(data[0].accountDisplayName).to.equal('my super user 3') | ||
142 | |||
143 | expect(data[0].channelDisplayName).to.equal('my user 3 channel') | ||
144 | expect(data[0].channelHandle).to.equal('super_user3_channel') | ||
145 | |||
146 | expect(data[0].createdAt).to.exist | ||
147 | expect(data[0].updatedAt).to.exist | ||
148 | |||
149 | expect(data[0].email).to.equal('user3@example.com') | ||
150 | expect(data[0].emailVerified).to.be.null | ||
151 | |||
152 | expect(data[0].moderationResponse).to.be.null | ||
153 | expect(data[0].registrationReason).to.equal('registration reason 3') | ||
154 | expect(data[0].state.id).to.equal(UserRegistrationState.PENDING) | ||
155 | expect(data[0].state.label).to.equal('Pending') | ||
156 | expect(data[0].user).to.be.null | ||
157 | } | ||
158 | |||
159 | { | ||
160 | expect(data[1].id).to.equal(id2) | ||
161 | expect(data[1].username).to.equal('user2') | ||
162 | expect(data[1].accountDisplayName).to.equal('my super user 2') | ||
163 | |||
164 | expect(data[1].channelDisplayName).to.be.null | ||
165 | expect(data[1].channelHandle).to.be.null | ||
166 | |||
167 | expect(data[1].createdAt).to.exist | ||
168 | expect(data[1].updatedAt).to.exist | ||
169 | |||
170 | expect(data[1].email).to.equal('user2@example.com') | ||
171 | expect(data[1].emailVerified).to.be.null | ||
172 | |||
173 | expect(data[1].moderationResponse).to.be.null | ||
174 | expect(data[1].registrationReason).to.equal('registration reason 2') | ||
175 | expect(data[1].state.id).to.equal(UserRegistrationState.PENDING) | ||
176 | expect(data[1].state.label).to.equal('Pending') | ||
177 | expect(data[1].user).to.be.null | ||
178 | } | ||
179 | |||
180 | { | ||
181 | expect(data[2].username).to.equal('user4') | ||
182 | } | ||
183 | } | ||
184 | |||
185 | { | ||
186 | const { total, data } = await server.registrations.list({ count: 1, start: 1, sort: 'createdAt' }) | ||
187 | |||
188 | expect(total).to.equal(3) | ||
189 | expect(data).to.have.lengthOf(1) | ||
190 | expect(data[0].id).to.equal(id2) | ||
191 | } | ||
192 | |||
193 | { | ||
194 | const { total, data } = await server.registrations.list({ search: 'user3' }) | ||
195 | expect(total).to.equal(1) | ||
196 | expect(data).to.have.lengthOf(1) | ||
197 | expect(data[0].id).to.equal(id3) | ||
198 | } | ||
199 | }) | ||
200 | |||
201 | it('Should reject a registration request', async function () { | ||
202 | await server.registrations.reject({ id: id4, moderationResponse: 'I do not want id 4 on this instance' }) | ||
203 | }) | ||
204 | |||
205 | it('Should have sent an email to the user explanining the registration has been rejected', async function () { | ||
206 | this.timeout(50000) | ||
207 | |||
208 | await waitJobs([ server ]) | ||
209 | |||
210 | const email = emails.find(e => e['to'][0]['address'] === 'user4@example.com') | ||
211 | expect(email).to.exist | ||
212 | |||
213 | expect(email['subject']).to.contain('been rejected') | ||
214 | expect(email['text']).to.contain('been rejected') | ||
215 | expect(email['text']).to.contain('I do not want id 4 on this instance') | ||
216 | }) | ||
217 | |||
218 | it('Should accept registration requests', async function () { | ||
219 | await server.registrations.accept({ id: id2, moderationResponse: 'Welcome id 2' }) | ||
220 | await server.registrations.accept({ id: id3, moderationResponse: 'Welcome id 3' }) | ||
221 | }) | ||
222 | |||
223 | it('Should have sent an email to the user explanining the registration has been accepted', async function () { | ||
224 | this.timeout(50000) | ||
225 | |||
226 | await waitJobs([ server ]) | ||
227 | |||
228 | { | ||
229 | const email = emails.find(e => e['to'][0]['address'] === 'user2@example.com') | ||
230 | expect(email).to.exist | ||
231 | |||
232 | expect(email['subject']).to.contain('been accepted') | ||
233 | expect(email['text']).to.contain('been accepted') | ||
234 | expect(email['text']).to.contain('Welcome id 2') | ||
235 | } | ||
236 | |||
237 | { | ||
238 | const email = emails.find(e => e['to'][0]['address'] === 'user3@example.com') | ||
239 | expect(email).to.exist | ||
240 | |||
241 | expect(email['subject']).to.contain('been accepted') | ||
242 | expect(email['text']).to.contain('been accepted') | ||
243 | expect(email['text']).to.contain('Welcome id 3') | ||
244 | } | ||
245 | }) | ||
246 | |||
247 | it('Should login with these users', async function () { | ||
248 | user2Token = await server.login.getAccessToken({ username: 'user2', password: 'user2password' }) | ||
249 | user3Token = await server.login.getAccessToken({ username: 'user3', password: 'user3password' }) | ||
250 | }) | ||
251 | |||
252 | it('Should have created the appropriate attributes for user 2', async function () { | ||
253 | const me = await server.users.getMyInfo({ token: user2Token }) | ||
254 | |||
255 | expect(me.username).to.equal('user2') | ||
256 | expect(me.account.displayName).to.equal('my super user 2') | ||
257 | expect(me.videoQuota).to.equal(5 * 1024 * 1024) | ||
258 | expect(me.videoChannels[0].name).to.equal('user2_channel') | ||
259 | expect(me.videoChannels[0].displayName).to.equal('Main user2 channel') | ||
260 | expect(me.role.id).to.equal(UserRole.USER) | ||
261 | expect(me.email).to.equal('user2@example.com') | ||
262 | }) | ||
263 | |||
264 | it('Should have created the appropriate attributes for user 3', async function () { | ||
265 | const me = await server.users.getMyInfo({ token: user3Token }) | ||
266 | |||
267 | expect(me.username).to.equal('user3') | ||
268 | expect(me.account.displayName).to.equal('my super user 3') | ||
269 | expect(me.videoQuota).to.equal(5 * 1024 * 1024) | ||
270 | expect(me.videoChannels[0].name).to.equal('super_user3_channel') | ||
271 | expect(me.videoChannels[0].displayName).to.equal('my user 3 channel') | ||
272 | expect(me.role.id).to.equal(UserRole.USER) | ||
273 | expect(me.email).to.equal('user3@example.com') | ||
274 | }) | ||
275 | |||
276 | it('Should list these accepted/rejected registration requests', async function () { | ||
277 | const { data } = await server.registrations.list({ sort: 'createdAt' }) | ||
278 | const { data: users } = await server.users.list() | ||
279 | |||
280 | { | ||
281 | expect(data[0].id).to.equal(id4) | ||
282 | expect(data[0].state.id).to.equal(UserRegistrationState.REJECTED) | ||
283 | expect(data[0].state.label).to.equal('Rejected') | ||
284 | |||
285 | expect(data[0].moderationResponse).to.equal('I do not want id 4 on this instance') | ||
286 | expect(data[0].user).to.be.null | ||
287 | |||
288 | expect(users.find(u => u.username === 'user4')).to.not.exist | ||
289 | } | ||
290 | |||
291 | { | ||
292 | expect(data[1].id).to.equal(id2) | ||
293 | expect(data[1].state.id).to.equal(UserRegistrationState.ACCEPTED) | ||
294 | expect(data[1].state.label).to.equal('Accepted') | ||
295 | |||
296 | expect(data[1].moderationResponse).to.equal('Welcome id 2') | ||
297 | expect(data[1].user).to.exist | ||
298 | |||
299 | const user2 = users.find(u => u.username === 'user2') | ||
300 | expect(data[1].user.id).to.equal(user2.id) | ||
301 | } | ||
302 | |||
303 | { | ||
304 | expect(data[2].id).to.equal(id3) | ||
305 | expect(data[2].state.id).to.equal(UserRegistrationState.ACCEPTED) | ||
306 | expect(data[2].state.label).to.equal('Accepted') | ||
307 | |||
308 | expect(data[2].moderationResponse).to.equal('Welcome id 3') | ||
309 | expect(data[2].user).to.exist | ||
310 | |||
311 | const user3 = users.find(u => u.username === 'user3') | ||
312 | expect(data[2].user.id).to.equal(user3.id) | ||
313 | } | ||
314 | }) | ||
315 | |||
316 | it('Shoulde delete a registration', async function () { | ||
317 | await server.registrations.delete({ id: id2 }) | ||
318 | await server.registrations.delete({ id: id3 }) | ||
319 | |||
320 | const { total, data } = await server.registrations.list() | ||
321 | expect(total).to.equal(1) | ||
322 | expect(data).to.have.lengthOf(1) | ||
323 | expect(data[0].id).to.equal(id4) | ||
324 | |||
325 | const { data: users } = await server.users.list() | ||
326 | |||
327 | for (const username of [ 'user2', 'user3' ]) { | ||
328 | expect(users.find(u => u.username === username)).to.exist | ||
329 | } | ||
330 | }) | ||
331 | |||
332 | it('Should be able to prevent email delivery on accept/reject', async function () { | ||
333 | this.timeout(50000) | ||
334 | |||
335 | let id1: number | ||
336 | let id2: number | ||
337 | |||
338 | { | ||
339 | const { id } = await server.registrations.requestRegistration({ | ||
340 | username: 'user7', | ||
341 | email: 'user7@example.com', | ||
342 | registrationReason: 'tt' | ||
343 | }) | ||
344 | id1 = id | ||
345 | } | ||
346 | { | ||
347 | const { id } = await server.registrations.requestRegistration({ | ||
348 | username: 'user8', | ||
349 | email: 'user8@example.com', | ||
350 | registrationReason: 'tt' | ||
351 | }) | ||
352 | id2 = id | ||
353 | } | ||
354 | |||
355 | await server.registrations.accept({ id: id1, moderationResponse: 'tt', preventEmailDelivery: true }) | ||
356 | await server.registrations.reject({ id: id2, moderationResponse: 'tt', preventEmailDelivery: true }) | ||
357 | |||
358 | await waitJobs([ server ]) | ||
359 | |||
360 | const filtered = emails.filter(e => { | ||
361 | const address = e['to'][0]['address'] | ||
362 | return address === 'user7@example.com' || address === 'user8@example.com' | ||
363 | }) | ||
364 | |||
365 | expect(filtered).to.have.lengthOf(0) | ||
366 | }) | ||
367 | |||
368 | it('Should request a registration without a channel, that will conflict with an already existing channel', async function () { | ||
369 | let id1: number | ||
370 | let id2: number | ||
371 | |||
372 | { | ||
373 | const { id } = await server.registrations.requestRegistration({ | ||
374 | registrationReason: 'tt', | ||
375 | username: 'user5', | ||
376 | password: 'user5password', | ||
377 | channel: { | ||
378 | displayName: 'channel 6', | ||
379 | name: 'user6_channel' | ||
380 | } | ||
381 | }) | ||
382 | |||
383 | id1 = id | ||
384 | } | ||
385 | |||
386 | { | ||
387 | const { id } = await server.registrations.requestRegistration({ | ||
388 | registrationReason: 'tt', | ||
389 | username: 'user6', | ||
390 | password: 'user6password' | ||
391 | }) | ||
392 | |||
393 | id2 = id | ||
394 | } | ||
395 | |||
396 | await server.registrations.accept({ id: id1, moderationResponse: 'tt' }) | ||
397 | await server.registrations.accept({ id: id2, moderationResponse: 'tt' }) | ||
398 | |||
399 | const user5Token = await server.login.getAccessToken('user5', 'user5password') | ||
400 | const user6Token = await server.login.getAccessToken('user6', 'user6password') | ||
401 | |||
402 | const user5 = await server.users.getMyInfo({ token: user5Token }) | ||
403 | const user6 = await server.users.getMyInfo({ token: user6Token }) | ||
404 | |||
405 | expect(user5.videoChannels[0].name).to.equal('user6_channel') | ||
406 | expect(user6.videoChannels[0].name).to.equal('user6_channel-1') | ||
407 | }) | ||
408 | }) | ||
409 | |||
410 | after(async function () { | ||
411 | MockSmtpServer.Instance.kill() | ||
412 | |||
413 | await cleanupTests([ server ]) | ||
414 | }) | ||
415 | }) | ||
diff --git a/packages/tests/src/api/users/two-factor.ts b/packages/tests/src/api/users/two-factor.ts new file mode 100644 index 000000000..fda125d20 --- /dev/null +++ b/packages/tests/src/api/users/two-factor.ts | |||
@@ -0,0 +1,206 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode, HttpStatusCodeType } from '@peertube/peertube-models' | ||
5 | import { expectStartWith } from '@tests/shared/checks.js' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | TwoFactorCommand | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | async function login (options: { | ||
15 | server: PeerTubeServer | ||
16 | username: string | ||
17 | password: string | ||
18 | otpToken?: string | ||
19 | expectedStatus?: HttpStatusCodeType | ||
20 | }) { | ||
21 | const { server, username, password, otpToken, expectedStatus } = options | ||
22 | |||
23 | const user = { username, password } | ||
24 | const { res, body: { access_token: token } } = await server.login.loginAndGetResponse({ user, otpToken, expectedStatus }) | ||
25 | |||
26 | return { res, token } | ||
27 | } | ||
28 | |||
29 | describe('Test users', function () { | ||
30 | let server: PeerTubeServer | ||
31 | let otpSecret: string | ||
32 | let requestToken: string | ||
33 | |||
34 | const userUsername = 'user1' | ||
35 | let userId: number | ||
36 | let userPassword: string | ||
37 | let userToken: string | ||
38 | |||
39 | before(async function () { | ||
40 | this.timeout(30000) | ||
41 | |||
42 | server = await createSingleServer(1) | ||
43 | |||
44 | await setAccessTokensToServers([ server ]) | ||
45 | const res = await server.users.generate(userUsername) | ||
46 | userId = res.userId | ||
47 | userPassword = res.password | ||
48 | userToken = res.token | ||
49 | }) | ||
50 | |||
51 | it('Should not add the header on login if two factor is not enabled', async function () { | ||
52 | const { res, token } = await login({ server, username: userUsername, password: userPassword }) | ||
53 | |||
54 | expect(res.header['x-peertube-otp']).to.not.exist | ||
55 | |||
56 | await server.users.getMyInfo({ token }) | ||
57 | }) | ||
58 | |||
59 | it('Should request two factor and get the secret and uri', async function () { | ||
60 | const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword }) | ||
61 | |||
62 | expect(otpRequest.requestToken).to.exist | ||
63 | |||
64 | expect(otpRequest.secret).to.exist | ||
65 | expect(otpRequest.secret).to.have.lengthOf(32) | ||
66 | |||
67 | expect(otpRequest.uri).to.exist | ||
68 | expectStartWith(otpRequest.uri, 'otpauth://') | ||
69 | expect(otpRequest.uri).to.include(otpRequest.secret) | ||
70 | |||
71 | requestToken = otpRequest.requestToken | ||
72 | otpSecret = otpRequest.secret | ||
73 | }) | ||
74 | |||
75 | it('Should not have two factor confirmed yet', async function () { | ||
76 | const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
77 | expect(twoFactorEnabled).to.be.false | ||
78 | }) | ||
79 | |||
80 | it('Should confirm two factor', async function () { | ||
81 | await server.twoFactor.confirmRequest({ | ||
82 | userId, | ||
83 | token: userToken, | ||
84 | otpToken: TwoFactorCommand.buildOTP({ secret: otpSecret }).generate(), | ||
85 | requestToken | ||
86 | }) | ||
87 | }) | ||
88 | |||
89 | it('Should not add the header on login if two factor is enabled and password is incorrect', async function () { | ||
90 | const { res, token } = await login({ server, username: userUsername, password: 'fake', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
91 | |||
92 | expect(res.header['x-peertube-otp']).to.not.exist | ||
93 | expect(token).to.not.exist | ||
94 | }) | ||
95 | |||
96 | it('Should add the header on login if two factor is enabled and password is correct', async function () { | ||
97 | const { res, token } = await login({ | ||
98 | server, | ||
99 | username: userUsername, | ||
100 | password: userPassword, | ||
101 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
102 | }) | ||
103 | |||
104 | expect(res.header['x-peertube-otp']).to.exist | ||
105 | expect(token).to.not.exist | ||
106 | |||
107 | await server.users.getMyInfo({ token }) | ||
108 | }) | ||
109 | |||
110 | it('Should not login with correct password and incorrect otp secret', async function () { | ||
111 | const otp = TwoFactorCommand.buildOTP({ secret: 'a'.repeat(32) }) | ||
112 | |||
113 | const { res, token } = await login({ | ||
114 | server, | ||
115 | username: userUsername, | ||
116 | password: userPassword, | ||
117 | otpToken: otp.generate(), | ||
118 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
119 | }) | ||
120 | |||
121 | expect(res.header['x-peertube-otp']).to.not.exist | ||
122 | expect(token).to.not.exist | ||
123 | }) | ||
124 | |||
125 | it('Should not login with correct password and incorrect otp code', async function () { | ||
126 | const { res, token } = await login({ | ||
127 | server, | ||
128 | username: userUsername, | ||
129 | password: userPassword, | ||
130 | otpToken: '123456', | ||
131 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
132 | }) | ||
133 | |||
134 | expect(res.header['x-peertube-otp']).to.not.exist | ||
135 | expect(token).to.not.exist | ||
136 | }) | ||
137 | |||
138 | it('Should not login with incorrect password and correct otp code', async function () { | ||
139 | const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate() | ||
140 | |||
141 | const { res, token } = await login({ | ||
142 | server, | ||
143 | username: userUsername, | ||
144 | password: 'fake', | ||
145 | otpToken, | ||
146 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
147 | }) | ||
148 | |||
149 | expect(res.header['x-peertube-otp']).to.not.exist | ||
150 | expect(token).to.not.exist | ||
151 | }) | ||
152 | |||
153 | it('Should correctly login with correct password and otp code', async function () { | ||
154 | const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate() | ||
155 | |||
156 | const { res, token } = await login({ server, username: userUsername, password: userPassword, otpToken }) | ||
157 | |||
158 | expect(res.header['x-peertube-otp']).to.not.exist | ||
159 | expect(token).to.exist | ||
160 | |||
161 | await server.users.getMyInfo({ token }) | ||
162 | }) | ||
163 | |||
164 | it('Should have two factor enabled when getting my info', async function () { | ||
165 | const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
166 | expect(twoFactorEnabled).to.be.true | ||
167 | }) | ||
168 | |||
169 | it('Should disable two factor and be able to login without otp token', async function () { | ||
170 | await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword }) | ||
171 | |||
172 | const { res, token } = await login({ server, username: userUsername, password: userPassword }) | ||
173 | expect(res.header['x-peertube-otp']).to.not.exist | ||
174 | |||
175 | await server.users.getMyInfo({ token }) | ||
176 | }) | ||
177 | |||
178 | it('Should have two factor disabled when getting my info', async function () { | ||
179 | const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
180 | expect(twoFactorEnabled).to.be.false | ||
181 | }) | ||
182 | |||
183 | it('Should enable two factor auth without password from an admin', async function () { | ||
184 | const { otpRequest } = await server.twoFactor.request({ userId }) | ||
185 | |||
186 | await server.twoFactor.confirmRequest({ | ||
187 | userId, | ||
188 | otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate(), | ||
189 | requestToken: otpRequest.requestToken | ||
190 | }) | ||
191 | |||
192 | const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
193 | expect(twoFactorEnabled).to.be.true | ||
194 | }) | ||
195 | |||
196 | it('Should disable two factor auth without password from an admin', async function () { | ||
197 | await server.twoFactor.disable({ userId }) | ||
198 | |||
199 | const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken }) | ||
200 | expect(twoFactorEnabled).to.be.false | ||
201 | }) | ||
202 | |||
203 | after(async function () { | ||
204 | await cleanupTests([ server ]) | ||
205 | }) | ||
206 | }) | ||
diff --git a/packages/tests/src/api/users/user-subscriptions.ts b/packages/tests/src/api/users/user-subscriptions.ts new file mode 100644 index 000000000..eb4ea9539 --- /dev/null +++ b/packages/tests/src/api/users/user-subscriptions.ts | |||
@@ -0,0 +1,614 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultAccountAvatar, | ||
12 | setDefaultChannelAvatar, | ||
13 | SubscriptionsCommand, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test users subscriptions', function () { | ||
18 | let servers: PeerTubeServer[] = [] | ||
19 | const users: { accessToken: string }[] = [] | ||
20 | let video3UUID: string | ||
21 | |||
22 | let command: SubscriptionsCommand | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(240000) | ||
26 | |||
27 | servers = await createMultipleServers(3) | ||
28 | |||
29 | // Get the access tokens | ||
30 | await setAccessTokensToServers(servers) | ||
31 | await setDefaultChannelAvatar(servers) | ||
32 | await setDefaultAccountAvatar(servers) | ||
33 | |||
34 | // Server 1 and server 2 follow each other | ||
35 | await doubleFollow(servers[0], servers[1]) | ||
36 | |||
37 | for (const server of servers) { | ||
38 | const user = { username: 'user' + server.serverNumber, password: 'password' } | ||
39 | await server.users.create({ username: user.username, password: user.password }) | ||
40 | |||
41 | const accessToken = await server.login.getAccessToken(user) | ||
42 | users.push({ accessToken }) | ||
43 | |||
44 | const videoName1 = 'video 1-' + server.serverNumber | ||
45 | await server.videos.upload({ token: accessToken, attributes: { name: videoName1 } }) | ||
46 | |||
47 | const videoName2 = 'video 2-' + server.serverNumber | ||
48 | await server.videos.upload({ token: accessToken, attributes: { name: videoName2 } }) | ||
49 | } | ||
50 | |||
51 | await waitJobs(servers) | ||
52 | |||
53 | command = servers[0].subscriptions | ||
54 | }) | ||
55 | |||
56 | describe('Destinction between server videos and user videos', function () { | ||
57 | it('Should display videos of server 2 on server 1', async function () { | ||
58 | const { total } = await servers[0].videos.list() | ||
59 | |||
60 | expect(total).to.equal(4) | ||
61 | }) | ||
62 | |||
63 | it('User of server 1 should follow user of server 3 and root of server 1', async function () { | ||
64 | this.timeout(60000) | ||
65 | |||
66 | await command.add({ token: users[0].accessToken, targetUri: 'user3_channel@' + servers[2].host }) | ||
67 | await command.add({ token: users[0].accessToken, targetUri: 'root_channel@' + servers[0].host }) | ||
68 | |||
69 | await waitJobs(servers) | ||
70 | |||
71 | const attributes = { name: 'video server 3 added after follow' } | ||
72 | const { uuid } = await servers[2].videos.upload({ token: users[2].accessToken, attributes }) | ||
73 | video3UUID = uuid | ||
74 | |||
75 | await waitJobs(servers) | ||
76 | }) | ||
77 | |||
78 | it('Should not display videos of server 3 on server 1', async function () { | ||
79 | const { total, data } = await servers[0].videos.list() | ||
80 | expect(total).to.equal(4) | ||
81 | |||
82 | for (const video of data) { | ||
83 | expect(video.name).to.not.contain('1-3') | ||
84 | expect(video.name).to.not.contain('2-3') | ||
85 | expect(video.name).to.not.contain('video server 3 added after follow') | ||
86 | } | ||
87 | }) | ||
88 | }) | ||
89 | |||
90 | describe('Subscription endpoints', function () { | ||
91 | |||
92 | it('Should list subscriptions', async function () { | ||
93 | { | ||
94 | const body = await command.list() | ||
95 | expect(body.total).to.equal(0) | ||
96 | expect(body.data).to.be.an('array') | ||
97 | expect(body.data).to.have.lengthOf(0) | ||
98 | } | ||
99 | |||
100 | { | ||
101 | const body = await command.list({ token: users[0].accessToken, sort: 'createdAt' }) | ||
102 | expect(body.total).to.equal(2) | ||
103 | |||
104 | const subscriptions = body.data | ||
105 | expect(subscriptions).to.be.an('array') | ||
106 | expect(subscriptions).to.have.lengthOf(2) | ||
107 | |||
108 | expect(subscriptions[0].name).to.equal('user3_channel') | ||
109 | expect(subscriptions[1].name).to.equal('root_channel') | ||
110 | } | ||
111 | }) | ||
112 | |||
113 | it('Should get subscription', async function () { | ||
114 | { | ||
115 | const videoChannel = await command.get({ token: users[0].accessToken, uri: 'user3_channel@' + servers[2].host }) | ||
116 | |||
117 | expect(videoChannel.name).to.equal('user3_channel') | ||
118 | expect(videoChannel.host).to.equal(servers[2].host) | ||
119 | expect(videoChannel.displayName).to.equal('Main user3 channel') | ||
120 | expect(videoChannel.followingCount).to.equal(0) | ||
121 | expect(videoChannel.followersCount).to.equal(1) | ||
122 | } | ||
123 | |||
124 | { | ||
125 | const videoChannel = await command.get({ token: users[0].accessToken, uri: 'root_channel@' + servers[0].host }) | ||
126 | |||
127 | expect(videoChannel.name).to.equal('root_channel') | ||
128 | expect(videoChannel.host).to.equal(servers[0].host) | ||
129 | expect(videoChannel.displayName).to.equal('Main root channel') | ||
130 | expect(videoChannel.followingCount).to.equal(0) | ||
131 | expect(videoChannel.followersCount).to.equal(1) | ||
132 | } | ||
133 | }) | ||
134 | |||
135 | it('Should return the existing subscriptions', async function () { | ||
136 | const uris = [ | ||
137 | 'user3_channel@' + servers[2].host, | ||
138 | 'root2_channel@' + servers[0].host, | ||
139 | 'root_channel@' + servers[0].host, | ||
140 | 'user3_channel@' + servers[0].host | ||
141 | ] | ||
142 | |||
143 | const body = await command.exist({ token: users[0].accessToken, uris }) | ||
144 | |||
145 | expect(body['user3_channel@' + servers[2].host]).to.be.true | ||
146 | expect(body['root2_channel@' + servers[0].host]).to.be.false | ||
147 | expect(body['root_channel@' + servers[0].host]).to.be.true | ||
148 | expect(body['user3_channel@' + servers[0].host]).to.be.false | ||
149 | }) | ||
150 | |||
151 | it('Should search among subscriptions', async function () { | ||
152 | { | ||
153 | const body = await command.list({ token: users[0].accessToken, sort: '-createdAt', search: 'user3_channel' }) | ||
154 | expect(body.total).to.equal(1) | ||
155 | expect(body.data).to.have.lengthOf(1) | ||
156 | } | ||
157 | |||
158 | { | ||
159 | const body = await command.list({ token: users[0].accessToken, sort: '-createdAt', search: 'toto' }) | ||
160 | expect(body.total).to.equal(0) | ||
161 | expect(body.data).to.have.lengthOf(0) | ||
162 | } | ||
163 | }) | ||
164 | }) | ||
165 | |||
166 | describe('Subscription videos', function () { | ||
167 | |||
168 | it('Should list subscription videos', async function () { | ||
169 | { | ||
170 | const body = await servers[0].videos.listMySubscriptionVideos() | ||
171 | expect(body.total).to.equal(0) | ||
172 | expect(body.data).to.be.an('array') | ||
173 | expect(body.data).to.have.lengthOf(0) | ||
174 | } | ||
175 | |||
176 | { | ||
177 | const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' }) | ||
178 | expect(body.total).to.equal(3) | ||
179 | |||
180 | const videos = body.data | ||
181 | expect(videos).to.be.an('array') | ||
182 | expect(videos).to.have.lengthOf(3) | ||
183 | |||
184 | expect(videos[0].name).to.equal('video 1-3') | ||
185 | expect(videos[1].name).to.equal('video 2-3') | ||
186 | expect(videos[2].name).to.equal('video server 3 added after follow') | ||
187 | } | ||
188 | |||
189 | { | ||
190 | const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, count: 1, start: 1 }) | ||
191 | expect(body.total).to.equal(3) | ||
192 | |||
193 | const videos = body.data | ||
194 | expect(videos).to.be.an('array') | ||
195 | expect(videos).to.have.lengthOf(1) | ||
196 | |||
197 | expect(videos[0].name).to.equal('video 2-3') | ||
198 | } | ||
199 | }) | ||
200 | |||
201 | it('Should upload a video by root on server 1 and see it in the subscription videos', async function () { | ||
202 | this.timeout(60000) | ||
203 | |||
204 | const videoName = 'video server 1 added after follow' | ||
205 | await servers[0].videos.upload({ attributes: { name: videoName } }) | ||
206 | |||
207 | await waitJobs(servers) | ||
208 | |||
209 | { | ||
210 | const body = await servers[0].videos.listMySubscriptionVideos() | ||
211 | expect(body.total).to.equal(0) | ||
212 | expect(body.data).to.be.an('array') | ||
213 | expect(body.data).to.have.lengthOf(0) | ||
214 | } | ||
215 | |||
216 | { | ||
217 | const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' }) | ||
218 | expect(body.total).to.equal(4) | ||
219 | |||
220 | const videos = body.data | ||
221 | expect(videos).to.be.an('array') | ||
222 | expect(videos).to.have.lengthOf(4) | ||
223 | |||
224 | expect(videos[0].name).to.equal('video 1-3') | ||
225 | expect(videos[1].name).to.equal('video 2-3') | ||
226 | expect(videos[2].name).to.equal('video server 3 added after follow') | ||
227 | expect(videos[3].name).to.equal('video server 1 added after follow') | ||
228 | } | ||
229 | |||
230 | { | ||
231 | const { data, total } = await servers[0].videos.list() | ||
232 | expect(total).to.equal(5) | ||
233 | |||
234 | for (const video of data) { | ||
235 | expect(video.name).to.not.contain('1-3') | ||
236 | expect(video.name).to.not.contain('2-3') | ||
237 | expect(video.name).to.not.contain('video server 3 added after follow') | ||
238 | } | ||
239 | } | ||
240 | }) | ||
241 | |||
242 | it('Should have server 1 following server 3 and display server 3 videos', async function () { | ||
243 | this.timeout(60000) | ||
244 | |||
245 | await servers[0].follows.follow({ hosts: [ servers[2].url ] }) | ||
246 | |||
247 | await waitJobs(servers) | ||
248 | |||
249 | const { data, total } = await servers[0].videos.list() | ||
250 | expect(total).to.equal(8) | ||
251 | |||
252 | const names = [ '1-3', '2-3', 'video server 3 added after follow' ] | ||
253 | for (const name of names) { | ||
254 | const video = data.find(v => v.name.includes(name)) | ||
255 | expect(video).to.not.be.undefined | ||
256 | } | ||
257 | }) | ||
258 | |||
259 | it('Should remove follow server 1 -> server 3 and hide server 3 videos', async function () { | ||
260 | this.timeout(60000) | ||
261 | |||
262 | await servers[0].follows.unfollow({ target: servers[2] }) | ||
263 | |||
264 | await waitJobs(servers) | ||
265 | |||
266 | const { total, data } = await servers[0].videos.list() | ||
267 | expect(total).to.equal(5) | ||
268 | |||
269 | for (const video of data) { | ||
270 | expect(video.name).to.not.contain('1-3') | ||
271 | expect(video.name).to.not.contain('2-3') | ||
272 | expect(video.name).to.not.contain('video server 3 added after follow') | ||
273 | } | ||
274 | }) | ||
275 | |||
276 | it('Should still list subscription videos', async function () { | ||
277 | { | ||
278 | const body = await servers[0].videos.listMySubscriptionVideos() | ||
279 | expect(body.total).to.equal(0) | ||
280 | expect(body.data).to.be.an('array') | ||
281 | expect(body.data).to.have.lengthOf(0) | ||
282 | } | ||
283 | |||
284 | { | ||
285 | const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' }) | ||
286 | expect(body.total).to.equal(4) | ||
287 | |||
288 | const videos = body.data | ||
289 | expect(videos).to.be.an('array') | ||
290 | expect(videos).to.have.lengthOf(4) | ||
291 | |||
292 | expect(videos[0].name).to.equal('video 1-3') | ||
293 | expect(videos[1].name).to.equal('video 2-3') | ||
294 | expect(videos[2].name).to.equal('video server 3 added after follow') | ||
295 | expect(videos[3].name).to.equal('video server 1 added after follow') | ||
296 | } | ||
297 | }) | ||
298 | }) | ||
299 | |||
300 | describe('Existing subscription video update', function () { | ||
301 | |||
302 | it('Should update a video of server 3 and see the updated video on server 1', async function () { | ||
303 | this.timeout(30000) | ||
304 | |||
305 | await servers[2].videos.update({ id: video3UUID, attributes: { name: 'video server 3 added after follow updated' } }) | ||
306 | |||
307 | await waitJobs(servers) | ||
308 | |||
309 | const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' }) | ||
310 | expect(body.data[2].name).to.equal('video server 3 added after follow updated') | ||
311 | }) | ||
312 | }) | ||
313 | |||
314 | describe('Subscription removal', function () { | ||
315 | |||
316 | it('Should remove user of server 3 subscription', async function () { | ||
317 | this.timeout(30000) | ||
318 | |||
319 | await command.remove({ token: users[0].accessToken, uri: 'user3_channel@' + servers[2].host }) | ||
320 | |||
321 | await waitJobs(servers) | ||
322 | }) | ||
323 | |||
324 | it('Should not display its videos anymore', async function () { | ||
325 | const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' }) | ||
326 | expect(body.total).to.equal(1) | ||
327 | |||
328 | const videos = body.data | ||
329 | expect(videos).to.be.an('array') | ||
330 | expect(videos).to.have.lengthOf(1) | ||
331 | |||
332 | expect(videos[0].name).to.equal('video server 1 added after follow') | ||
333 | }) | ||
334 | |||
335 | it('Should remove the root subscription and not display the videos anymore', async function () { | ||
336 | this.timeout(30000) | ||
337 | |||
338 | await command.remove({ token: users[0].accessToken, uri: 'root_channel@' + servers[0].host }) | ||
339 | |||
340 | await waitJobs(servers) | ||
341 | |||
342 | { | ||
343 | const body = await command.list({ token: users[0].accessToken, sort: 'createdAt' }) | ||
344 | expect(body.total).to.equal(0) | ||
345 | |||
346 | const videos = body.data | ||
347 | expect(videos).to.be.an('array') | ||
348 | expect(videos).to.have.lengthOf(0) | ||
349 | } | ||
350 | }) | ||
351 | |||
352 | it('Should correctly display public videos on server 1', async function () { | ||
353 | const { total, data } = await servers[0].videos.list() | ||
354 | expect(total).to.equal(5) | ||
355 | |||
356 | for (const video of data) { | ||
357 | expect(video.name).to.not.contain('1-3') | ||
358 | expect(video.name).to.not.contain('2-3') | ||
359 | expect(video.name).to.not.contain('video server 3 added after follow updated') | ||
360 | } | ||
361 | }) | ||
362 | }) | ||
363 | |||
364 | describe('Re-follow', function () { | ||
365 | |||
366 | it('Should follow user of server 3 again', async function () { | ||
367 | this.timeout(60000) | ||
368 | |||
369 | await command.add({ token: users[0].accessToken, targetUri: 'user3_channel@' + servers[2].host }) | ||
370 | |||
371 | await waitJobs(servers) | ||
372 | |||
373 | { | ||
374 | const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' }) | ||
375 | expect(body.total).to.equal(3) | ||
376 | |||
377 | const videos = body.data | ||
378 | expect(videos).to.be.an('array') | ||
379 | expect(videos).to.have.lengthOf(3) | ||
380 | |||
381 | expect(videos[0].name).to.equal('video 1-3') | ||
382 | expect(videos[1].name).to.equal('video 2-3') | ||
383 | expect(videos[2].name).to.equal('video server 3 added after follow updated') | ||
384 | } | ||
385 | |||
386 | { | ||
387 | const { total, data } = await servers[0].videos.list() | ||
388 | expect(total).to.equal(5) | ||
389 | |||
390 | for (const video of data) { | ||
391 | expect(video.name).to.not.contain('1-3') | ||
392 | expect(video.name).to.not.contain('2-3') | ||
393 | expect(video.name).to.not.contain('video server 3 added after follow updated') | ||
394 | } | ||
395 | } | ||
396 | }) | ||
397 | |||
398 | it('Should follow user channels of server 3 by root of server 3', async function () { | ||
399 | this.timeout(60000) | ||
400 | |||
401 | await servers[2].channels.create({ token: users[2].accessToken, attributes: { name: 'user3_channel2' } }) | ||
402 | |||
403 | await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel@' + servers[2].host }) | ||
404 | await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel2@' + servers[2].host }) | ||
405 | |||
406 | await waitJobs(servers) | ||
407 | }) | ||
408 | }) | ||
409 | |||
410 | describe('Followers listing', function () { | ||
411 | |||
412 | it('Should list user 3 followers', async function () { | ||
413 | { | ||
414 | const { total, data } = await servers[2].accounts.listFollowers({ | ||
415 | token: users[2].accessToken, | ||
416 | accountName: 'user3', | ||
417 | start: 0, | ||
418 | count: 5, | ||
419 | sort: 'createdAt' | ||
420 | }) | ||
421 | |||
422 | expect(total).to.equal(3) | ||
423 | expect(data).to.have.lengthOf(3) | ||
424 | |||
425 | expect(data[0].following.host).to.equal(servers[2].host) | ||
426 | expect(data[0].following.name).to.equal('user3_channel') | ||
427 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
428 | expect(data[0].follower.name).to.equal('user1') | ||
429 | |||
430 | expect(data[1].following.host).to.equal(servers[2].host) | ||
431 | expect(data[1].following.name).to.equal('user3_channel') | ||
432 | expect(data[1].follower.host).to.equal(servers[2].host) | ||
433 | expect(data[1].follower.name).to.equal('root') | ||
434 | |||
435 | expect(data[2].following.host).to.equal(servers[2].host) | ||
436 | expect(data[2].following.name).to.equal('user3_channel2') | ||
437 | expect(data[2].follower.host).to.equal(servers[2].host) | ||
438 | expect(data[2].follower.name).to.equal('root') | ||
439 | } | ||
440 | |||
441 | { | ||
442 | const { total, data } = await servers[2].accounts.listFollowers({ | ||
443 | token: users[2].accessToken, | ||
444 | accountName: 'user3', | ||
445 | start: 0, | ||
446 | count: 1, | ||
447 | sort: '-createdAt' | ||
448 | }) | ||
449 | |||
450 | expect(total).to.equal(3) | ||
451 | expect(data).to.have.lengthOf(1) | ||
452 | |||
453 | expect(data[0].following.host).to.equal(servers[2].host) | ||
454 | expect(data[0].following.name).to.equal('user3_channel2') | ||
455 | expect(data[0].follower.host).to.equal(servers[2].host) | ||
456 | expect(data[0].follower.name).to.equal('root') | ||
457 | } | ||
458 | |||
459 | { | ||
460 | const { total, data } = await servers[2].accounts.listFollowers({ | ||
461 | token: users[2].accessToken, | ||
462 | accountName: 'user3', | ||
463 | start: 1, | ||
464 | count: 1, | ||
465 | sort: '-createdAt' | ||
466 | }) | ||
467 | |||
468 | expect(total).to.equal(3) | ||
469 | expect(data).to.have.lengthOf(1) | ||
470 | |||
471 | expect(data[0].following.host).to.equal(servers[2].host) | ||
472 | expect(data[0].following.name).to.equal('user3_channel') | ||
473 | expect(data[0].follower.host).to.equal(servers[2].host) | ||
474 | expect(data[0].follower.name).to.equal('root') | ||
475 | } | ||
476 | |||
477 | { | ||
478 | const { total, data } = await servers[2].accounts.listFollowers({ | ||
479 | token: users[2].accessToken, | ||
480 | accountName: 'user3', | ||
481 | search: 'user1', | ||
482 | sort: '-createdAt' | ||
483 | }) | ||
484 | |||
485 | expect(total).to.equal(1) | ||
486 | expect(data).to.have.lengthOf(1) | ||
487 | |||
488 | expect(data[0].following.host).to.equal(servers[2].host) | ||
489 | expect(data[0].following.name).to.equal('user3_channel') | ||
490 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
491 | expect(data[0].follower.name).to.equal('user1') | ||
492 | } | ||
493 | }) | ||
494 | |||
495 | it('Should list user3_channel followers', async function () { | ||
496 | { | ||
497 | const { total, data } = await servers[2].channels.listFollowers({ | ||
498 | token: users[2].accessToken, | ||
499 | channelName: 'user3_channel', | ||
500 | start: 0, | ||
501 | count: 5, | ||
502 | sort: 'createdAt' | ||
503 | }) | ||
504 | |||
505 | expect(total).to.equal(2) | ||
506 | expect(data).to.have.lengthOf(2) | ||
507 | |||
508 | expect(data[0].following.host).to.equal(servers[2].host) | ||
509 | expect(data[0].following.name).to.equal('user3_channel') | ||
510 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
511 | expect(data[0].follower.name).to.equal('user1') | ||
512 | |||
513 | expect(data[1].following.host).to.equal(servers[2].host) | ||
514 | expect(data[1].following.name).to.equal('user3_channel') | ||
515 | expect(data[1].follower.host).to.equal(servers[2].host) | ||
516 | expect(data[1].follower.name).to.equal('root') | ||
517 | } | ||
518 | |||
519 | { | ||
520 | const { total, data } = await servers[2].channels.listFollowers({ | ||
521 | token: users[2].accessToken, | ||
522 | channelName: 'user3_channel', | ||
523 | start: 0, | ||
524 | count: 1, | ||
525 | sort: '-createdAt' | ||
526 | }) | ||
527 | |||
528 | expect(total).to.equal(2) | ||
529 | expect(data).to.have.lengthOf(1) | ||
530 | |||
531 | expect(data[0].following.host).to.equal(servers[2].host) | ||
532 | expect(data[0].following.name).to.equal('user3_channel') | ||
533 | expect(data[0].follower.host).to.equal(servers[2].host) | ||
534 | expect(data[0].follower.name).to.equal('root') | ||
535 | } | ||
536 | |||
537 | { | ||
538 | const { total, data } = await servers[2].channels.listFollowers({ | ||
539 | token: users[2].accessToken, | ||
540 | channelName: 'user3_channel', | ||
541 | start: 1, | ||
542 | count: 1, | ||
543 | sort: '-createdAt' | ||
544 | }) | ||
545 | |||
546 | expect(total).to.equal(2) | ||
547 | expect(data).to.have.lengthOf(1) | ||
548 | |||
549 | expect(data[0].following.host).to.equal(servers[2].host) | ||
550 | expect(data[0].following.name).to.equal('user3_channel') | ||
551 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
552 | expect(data[0].follower.name).to.equal('user1') | ||
553 | } | ||
554 | |||
555 | { | ||
556 | const { total, data } = await servers[2].channels.listFollowers({ | ||
557 | token: users[2].accessToken, | ||
558 | channelName: 'user3_channel', | ||
559 | search: 'user1', | ||
560 | sort: '-createdAt' | ||
561 | }) | ||
562 | |||
563 | expect(total).to.equal(1) | ||
564 | expect(data).to.have.lengthOf(1) | ||
565 | |||
566 | expect(data[0].following.host).to.equal(servers[2].host) | ||
567 | expect(data[0].following.name).to.equal('user3_channel') | ||
568 | expect(data[0].follower.host).to.equal(servers[0].host) | ||
569 | expect(data[0].follower.name).to.equal('user1') | ||
570 | } | ||
571 | }) | ||
572 | }) | ||
573 | |||
574 | describe('Subscription videos privacy', function () { | ||
575 | |||
576 | it('Should update video as internal and not see from remote server', async function () { | ||
577 | this.timeout(30000) | ||
578 | |||
579 | await servers[2].videos.update({ id: video3UUID, attributes: { name: 'internal', privacy: VideoPrivacy.INTERNAL } }) | ||
580 | await waitJobs(servers) | ||
581 | |||
582 | { | ||
583 | const { data } = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken }) | ||
584 | expect(data.find(v => v.name === 'internal')).to.not.exist | ||
585 | } | ||
586 | }) | ||
587 | |||
588 | it('Should see internal from local user', async function () { | ||
589 | const { data } = await servers[2].videos.listMySubscriptionVideos({ token: servers[2].accessToken }) | ||
590 | expect(data.find(v => v.name === 'internal')).to.exist | ||
591 | }) | ||
592 | |||
593 | it('Should update video as private and not see from anyone server', async function () { | ||
594 | this.timeout(30000) | ||
595 | |||
596 | await servers[2].videos.update({ id: video3UUID, attributes: { name: 'private', privacy: VideoPrivacy.PRIVATE } }) | ||
597 | await waitJobs(servers) | ||
598 | |||
599 | { | ||
600 | const { data } = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken }) | ||
601 | expect(data.find(v => v.name === 'private')).to.not.exist | ||
602 | } | ||
603 | |||
604 | { | ||
605 | const { data } = await servers[2].videos.listMySubscriptionVideos({ token: servers[2].accessToken }) | ||
606 | expect(data.find(v => v.name === 'private')).to.not.exist | ||
607 | } | ||
608 | }) | ||
609 | }) | ||
610 | |||
611 | after(async function () { | ||
612 | await cleanupTests(servers) | ||
613 | }) | ||
614 | }) | ||
diff --git a/packages/tests/src/api/users/user-videos.ts b/packages/tests/src/api/users/user-videos.ts new file mode 100644 index 000000000..7b075d040 --- /dev/null +++ b/packages/tests/src/api/users/user-videos.ts | |||
@@ -0,0 +1,219 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test user videos', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let videoId: number | ||
18 | let videoId2: number | ||
19 | let token: string | ||
20 | let anotherUserToken: string | ||
21 | |||
22 | before(async function () { | ||
23 | this.timeout(120000) | ||
24 | |||
25 | server = await createSingleServer(1) | ||
26 | |||
27 | await setAccessTokensToServers([ server ]) | ||
28 | await setDefaultChannelAvatar([ server ]) | ||
29 | await setDefaultAccountAvatar([ server ]) | ||
30 | |||
31 | await server.videos.quickUpload({ name: 'root video' }) | ||
32 | await server.videos.quickUpload({ name: 'root video 2' }) | ||
33 | |||
34 | token = await server.users.generateUserAndToken('user') | ||
35 | anotherUserToken = await server.users.generateUserAndToken('user2') | ||
36 | }) | ||
37 | |||
38 | describe('List my videos', function () { | ||
39 | |||
40 | it('Should list my videos', async function () { | ||
41 | const { data, total } = await server.videos.listMyVideos() | ||
42 | |||
43 | expect(total).to.equal(2) | ||
44 | expect(data).to.have.lengthOf(2) | ||
45 | }) | ||
46 | }) | ||
47 | |||
48 | describe('Upload', function () { | ||
49 | |||
50 | it('Should upload the video with the correct token', async function () { | ||
51 | await server.videos.upload({ token }) | ||
52 | const { data } = await server.videos.list() | ||
53 | const video = data[0] | ||
54 | |||
55 | expect(video.account.name).to.equal('user') | ||
56 | videoId = video.id | ||
57 | }) | ||
58 | |||
59 | it('Should upload the video again with the correct token', async function () { | ||
60 | const { id } = await server.videos.upload({ token }) | ||
61 | videoId2 = id | ||
62 | }) | ||
63 | }) | ||
64 | |||
65 | describe('Ratings', function () { | ||
66 | |||
67 | it('Should retrieve a video rating', async function () { | ||
68 | await server.videos.rate({ id: videoId, token, rating: 'like' }) | ||
69 | const rating = await server.users.getMyRating({ token, videoId }) | ||
70 | |||
71 | expect(rating.videoId).to.equal(videoId) | ||
72 | expect(rating.rating).to.equal('like') | ||
73 | }) | ||
74 | |||
75 | it('Should retrieve ratings list', async function () { | ||
76 | await server.videos.rate({ id: videoId, token, rating: 'like' }) | ||
77 | |||
78 | const body = await server.accounts.listRatings({ accountName: 'user', token }) | ||
79 | |||
80 | expect(body.total).to.equal(1) | ||
81 | expect(body.data[0].video.id).to.equal(videoId) | ||
82 | expect(body.data[0].rating).to.equal('like') | ||
83 | }) | ||
84 | |||
85 | it('Should retrieve ratings list by rating type', async function () { | ||
86 | { | ||
87 | const body = await server.accounts.listRatings({ accountName: 'user', token, rating: 'like' }) | ||
88 | expect(body.data.length).to.equal(1) | ||
89 | } | ||
90 | |||
91 | { | ||
92 | const body = await server.accounts.listRatings({ accountName: 'user', token, rating: 'dislike' }) | ||
93 | expect(body.data.length).to.equal(0) | ||
94 | } | ||
95 | }) | ||
96 | }) | ||
97 | |||
98 | describe('Remove video', function () { | ||
99 | |||
100 | it('Should not be able to remove the video with an incorrect token', async function () { | ||
101 | await server.videos.remove({ token: 'bad_token', id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
102 | }) | ||
103 | |||
104 | it('Should not be able to remove the video with the token of another account', async function () { | ||
105 | await server.videos.remove({ token: anotherUserToken, id: videoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
106 | }) | ||
107 | |||
108 | it('Should be able to remove the video with the correct token', async function () { | ||
109 | await server.videos.remove({ token, id: videoId }) | ||
110 | await server.videos.remove({ token, id: videoId2 }) | ||
111 | }) | ||
112 | }) | ||
113 | |||
114 | describe('My videos & quotas', function () { | ||
115 | |||
116 | it('Should be able to upload a video with a user', async function () { | ||
117 | this.timeout(30000) | ||
118 | |||
119 | const attributes = { | ||
120 | name: 'super user video', | ||
121 | fixture: 'video_short.webm' | ||
122 | } | ||
123 | await server.videos.upload({ token, attributes }) | ||
124 | |||
125 | await server.channels.create({ token, attributes: { name: 'other_channel' } }) | ||
126 | }) | ||
127 | |||
128 | it('Should have video quota updated', async function () { | ||
129 | const quota = await server.users.getMyQuotaUsed({ token }) | ||
130 | expect(quota.videoQuotaUsed).to.equal(218910) | ||
131 | expect(quota.videoQuotaUsedDaily).to.equal(218910) | ||
132 | |||
133 | const { data } = await server.users.list() | ||
134 | const tmpUser = data.find(u => u.username === 'user') | ||
135 | expect(tmpUser.videoQuotaUsed).to.equal(218910) | ||
136 | expect(tmpUser.videoQuotaUsedDaily).to.equal(218910) | ||
137 | }) | ||
138 | |||
139 | it('Should be able to list my videos', async function () { | ||
140 | const { total, data } = await server.videos.listMyVideos({ token }) | ||
141 | expect(total).to.equal(1) | ||
142 | expect(data).to.have.lengthOf(1) | ||
143 | |||
144 | const video = data[0] | ||
145 | expect(video.name).to.equal('super user video') | ||
146 | expect(video.thumbnailPath).to.not.be.null | ||
147 | expect(video.previewPath).to.not.be.null | ||
148 | }) | ||
149 | |||
150 | it('Should be able to filter by channel in my videos', async function () { | ||
151 | const myInfo = await server.users.getMyInfo({ token }) | ||
152 | const mainChannel = myInfo.videoChannels.find(c => c.name !== 'other_channel') | ||
153 | const otherChannel = myInfo.videoChannels.find(c => c.name === 'other_channel') | ||
154 | |||
155 | { | ||
156 | const { total, data } = await server.videos.listMyVideos({ token, channelId: mainChannel.id }) | ||
157 | expect(total).to.equal(1) | ||
158 | expect(data).to.have.lengthOf(1) | ||
159 | |||
160 | const video = data[0] | ||
161 | expect(video.name).to.equal('super user video') | ||
162 | expect(video.thumbnailPath).to.not.be.null | ||
163 | expect(video.previewPath).to.not.be.null | ||
164 | } | ||
165 | |||
166 | { | ||
167 | const { total, data } = await server.videos.listMyVideos({ token, channelId: otherChannel.id }) | ||
168 | expect(total).to.equal(0) | ||
169 | expect(data).to.have.lengthOf(0) | ||
170 | } | ||
171 | }) | ||
172 | |||
173 | it('Should be able to search in my videos', async function () { | ||
174 | { | ||
175 | const { total, data } = await server.videos.listMyVideos({ token, sort: '-createdAt', search: 'user video' }) | ||
176 | expect(total).to.equal(1) | ||
177 | expect(data).to.have.lengthOf(1) | ||
178 | } | ||
179 | |||
180 | { | ||
181 | const { total, data } = await server.videos.listMyVideos({ token, sort: '-createdAt', search: 'toto' }) | ||
182 | expect(total).to.equal(0) | ||
183 | expect(data).to.have.lengthOf(0) | ||
184 | } | ||
185 | }) | ||
186 | |||
187 | it('Should disable web videos, enable HLS, and update my quota', async function () { | ||
188 | this.timeout(160000) | ||
189 | |||
190 | { | ||
191 | const config = await server.config.getCustomConfig() | ||
192 | config.transcoding.webVideos.enabled = false | ||
193 | config.transcoding.hls.enabled = true | ||
194 | config.transcoding.enabled = true | ||
195 | await server.config.updateCustomSubConfig({ newConfig: config }) | ||
196 | } | ||
197 | |||
198 | { | ||
199 | const attributes = { | ||
200 | name: 'super user video 2', | ||
201 | fixture: 'video_short.webm' | ||
202 | } | ||
203 | await server.videos.upload({ token, attributes }) | ||
204 | |||
205 | await waitJobs([ server ]) | ||
206 | } | ||
207 | |||
208 | { | ||
209 | const data = await server.users.getMyQuotaUsed({ token }) | ||
210 | expect(data.videoQuotaUsed).to.be.greaterThan(220000) | ||
211 | expect(data.videoQuotaUsedDaily).to.be.greaterThan(220000) | ||
212 | } | ||
213 | }) | ||
214 | }) | ||
215 | |||
216 | after(async function () { | ||
217 | await cleanupTests([ server ]) | ||
218 | }) | ||
219 | }) | ||
diff --git a/packages/tests/src/api/users/users-email-verification.ts b/packages/tests/src/api/users/users-email-verification.ts new file mode 100644 index 000000000..689e3c4bb --- /dev/null +++ b/packages/tests/src/api/users/users-email-verification.ts | |||
@@ -0,0 +1,165 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test users email verification', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let userId: number | ||
18 | let userAccessToken: string | ||
19 | let verificationString: string | ||
20 | let expectedEmailsLength = 0 | ||
21 | const user1 = { | ||
22 | username: 'user_1', | ||
23 | password: 'super password' | ||
24 | } | ||
25 | const user2 = { | ||
26 | username: 'user_2', | ||
27 | password: 'super password' | ||
28 | } | ||
29 | const emails: object[] = [] | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(30000) | ||
33 | |||
34 | const port = await MockSmtpServer.Instance.collectEmails(emails) | ||
35 | server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(port)) | ||
36 | |||
37 | await setAccessTokensToServers([ server ]) | ||
38 | }) | ||
39 | |||
40 | it('Should register user and send verification email if verification required', async function () { | ||
41 | this.timeout(30000) | ||
42 | |||
43 | await server.config.updateExistingSubConfig({ | ||
44 | newConfig: { | ||
45 | signup: { | ||
46 | enabled: true, | ||
47 | requiresApproval: false, | ||
48 | requiresEmailVerification: true, | ||
49 | limit: 10 | ||
50 | } | ||
51 | } | ||
52 | }) | ||
53 | |||
54 | await server.registrations.register(user1) | ||
55 | |||
56 | await waitJobs(server) | ||
57 | expectedEmailsLength++ | ||
58 | expect(emails).to.have.lengthOf(expectedEmailsLength) | ||
59 | |||
60 | const email = emails[expectedEmailsLength - 1] | ||
61 | |||
62 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
63 | expect(verificationStringMatches).not.to.be.null | ||
64 | |||
65 | verificationString = verificationStringMatches[1] | ||
66 | expect(verificationString).to.have.length.above(2) | ||
67 | |||
68 | const userIdMatches = /userId=([0-9]+)/.exec(email['text']) | ||
69 | expect(userIdMatches).not.to.be.null | ||
70 | |||
71 | userId = parseInt(userIdMatches[1], 10) | ||
72 | |||
73 | const body = await server.users.get({ userId }) | ||
74 | expect(body.emailVerified).to.be.false | ||
75 | }) | ||
76 | |||
77 | it('Should not allow login for user with unverified email', async function () { | ||
78 | const { detail } = await server.login.login({ user: user1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
79 | expect(detail).to.contain('User email is not verified.') | ||
80 | }) | ||
81 | |||
82 | it('Should verify the user via email and allow login', async function () { | ||
83 | await server.users.verifyEmail({ userId, verificationString }) | ||
84 | |||
85 | const body = await server.login.login({ user: user1 }) | ||
86 | userAccessToken = body.access_token | ||
87 | |||
88 | const user = await server.users.get({ userId }) | ||
89 | expect(user.emailVerified).to.be.true | ||
90 | }) | ||
91 | |||
92 | it('Should be able to change the user email', async function () { | ||
93 | let updateVerificationString: string | ||
94 | |||
95 | { | ||
96 | await server.users.updateMe({ | ||
97 | token: userAccessToken, | ||
98 | email: 'updated@example.com', | ||
99 | currentPassword: user1.password | ||
100 | }) | ||
101 | |||
102 | await waitJobs(server) | ||
103 | expectedEmailsLength++ | ||
104 | expect(emails).to.have.lengthOf(expectedEmailsLength) | ||
105 | |||
106 | const email = emails[expectedEmailsLength - 1] | ||
107 | |||
108 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
109 | updateVerificationString = verificationStringMatches[1] | ||
110 | } | ||
111 | |||
112 | { | ||
113 | const me = await server.users.getMyInfo({ token: userAccessToken }) | ||
114 | expect(me.email).to.equal('user_1@example.com') | ||
115 | expect(me.pendingEmail).to.equal('updated@example.com') | ||
116 | } | ||
117 | |||
118 | { | ||
119 | await server.users.verifyEmail({ userId, verificationString: updateVerificationString, isPendingEmail: true }) | ||
120 | |||
121 | const me = await server.users.getMyInfo({ token: userAccessToken }) | ||
122 | expect(me.email).to.equal('updated@example.com') | ||
123 | expect(me.pendingEmail).to.be.null | ||
124 | } | ||
125 | }) | ||
126 | |||
127 | it('Should register user not requiring email verification if setting not enabled', async function () { | ||
128 | this.timeout(5000) | ||
129 | await server.config.updateExistingSubConfig({ | ||
130 | newConfig: { | ||
131 | signup: { | ||
132 | requiresEmailVerification: false | ||
133 | } | ||
134 | } | ||
135 | }) | ||
136 | |||
137 | await server.registrations.register(user2) | ||
138 | |||
139 | await waitJobs(server) | ||
140 | expect(emails).to.have.lengthOf(expectedEmailsLength) | ||
141 | |||
142 | const accessToken = await server.login.getAccessToken(user2) | ||
143 | |||
144 | const user = await server.users.getMyInfo({ token: accessToken }) | ||
145 | expect(user.emailVerified).to.be.null | ||
146 | }) | ||
147 | |||
148 | it('Should allow login for user with unverified email when setting later enabled', async function () { | ||
149 | await server.config.updateCustomSubConfig({ | ||
150 | newConfig: { | ||
151 | signup: { | ||
152 | requiresEmailVerification: true | ||
153 | } | ||
154 | } | ||
155 | }) | ||
156 | |||
157 | await server.login.getAccessToken(user2) | ||
158 | }) | ||
159 | |||
160 | after(async function () { | ||
161 | MockSmtpServer.Instance.kill() | ||
162 | |||
163 | await cleanupTests([ server ]) | ||
164 | }) | ||
165 | }) | ||
diff --git a/packages/tests/src/api/users/users-multiple-servers.ts b/packages/tests/src/api/users/users-multiple-servers.ts new file mode 100644 index 000000000..61e3aa001 --- /dev/null +++ b/packages/tests/src/api/users/users-multiple-servers.ts | |||
@@ -0,0 +1,213 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { MyUser } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultChannelAvatar, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | import { checkActorFilesWereRemoved } from '@tests/shared/actors.js' | ||
15 | import { testImage } from '@tests/shared/checks.js' | ||
16 | import { checkTmpIsEmpty } from '@tests/shared/directories.js' | ||
17 | import { saveVideoInServers, checkVideoFilesWereRemoved } from '@tests/shared/videos.js' | ||
18 | |||
19 | describe('Test users with multiple servers', function () { | ||
20 | let servers: PeerTubeServer[] = [] | ||
21 | |||
22 | let user: MyUser | ||
23 | let userId: number | ||
24 | |||
25 | let videoUUID: string | ||
26 | let userAccessToken: string | ||
27 | let userAvatarFilenames: string[] | ||
28 | |||
29 | before(async function () { | ||
30 | this.timeout(120_000) | ||
31 | |||
32 | servers = await createMultipleServers(3) | ||
33 | |||
34 | // Get the access tokens | ||
35 | await setAccessTokensToServers(servers) | ||
36 | await setDefaultChannelAvatar(servers) | ||
37 | |||
38 | // Server 1 and server 2 follow each other | ||
39 | await doubleFollow(servers[0], servers[1]) | ||
40 | // Server 1 and server 3 follow each other | ||
41 | await doubleFollow(servers[0], servers[2]) | ||
42 | // Server 2 and server 3 follow each other | ||
43 | await doubleFollow(servers[1], servers[2]) | ||
44 | |||
45 | // The root user of server 1 is propagated to servers 2 and 3 | ||
46 | await servers[0].videos.upload() | ||
47 | |||
48 | { | ||
49 | const username = 'user1' | ||
50 | const created = await servers[0].users.create({ username }) | ||
51 | userId = created.id | ||
52 | userAccessToken = await servers[0].login.getAccessToken(username) | ||
53 | } | ||
54 | |||
55 | { | ||
56 | const { uuid } = await servers[0].videos.upload({ token: userAccessToken }) | ||
57 | videoUUID = uuid | ||
58 | |||
59 | await waitJobs(servers) | ||
60 | |||
61 | await saveVideoInServers(servers, videoUUID) | ||
62 | } | ||
63 | }) | ||
64 | |||
65 | it('Should be able to update my display name', async function () { | ||
66 | await servers[0].users.updateMe({ displayName: 'my super display name' }) | ||
67 | |||
68 | user = await servers[0].users.getMyInfo() | ||
69 | expect(user.account.displayName).to.equal('my super display name') | ||
70 | |||
71 | await waitJobs(servers) | ||
72 | }) | ||
73 | |||
74 | it('Should be able to update my description', async function () { | ||
75 | this.timeout(10_000) | ||
76 | |||
77 | await servers[0].users.updateMe({ description: 'my super description updated' }) | ||
78 | |||
79 | user = await servers[0].users.getMyInfo() | ||
80 | expect(user.account.displayName).to.equal('my super display name') | ||
81 | expect(user.account.description).to.equal('my super description updated') | ||
82 | |||
83 | await waitJobs(servers) | ||
84 | }) | ||
85 | |||
86 | it('Should be able to update my avatar', async function () { | ||
87 | this.timeout(10_000) | ||
88 | |||
89 | const fixture = 'avatar2.png' | ||
90 | |||
91 | await servers[0].users.updateMyAvatar({ fixture }) | ||
92 | |||
93 | user = await servers[0].users.getMyInfo() | ||
94 | userAvatarFilenames = user.account.avatars.map(({ path }) => path) | ||
95 | |||
96 | for (const avatar of user.account.avatars) { | ||
97 | await testImage(servers[0].url, `avatar2-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
98 | } | ||
99 | |||
100 | await waitJobs(servers) | ||
101 | }) | ||
102 | |||
103 | it('Should have updated my profile on other servers too', async function () { | ||
104 | let createdAt: string | Date | ||
105 | |||
106 | for (const server of servers) { | ||
107 | const body = await server.accounts.list({ sort: '-createdAt' }) | ||
108 | |||
109 | const resList = body.data.find(a => a.name === 'root' && a.host === servers[0].host) | ||
110 | expect(resList).not.to.be.undefined | ||
111 | |||
112 | const account = await server.accounts.get({ accountName: resList.name + '@' + resList.host }) | ||
113 | |||
114 | if (!createdAt) createdAt = account.createdAt | ||
115 | |||
116 | expect(account.name).to.equal('root') | ||
117 | expect(account.host).to.equal(servers[0].host) | ||
118 | expect(account.displayName).to.equal('my super display name') | ||
119 | expect(account.description).to.equal('my super description updated') | ||
120 | expect(createdAt).to.equal(account.createdAt) | ||
121 | |||
122 | if (server.serverNumber === 1) { | ||
123 | expect(account.userId).to.be.a('number') | ||
124 | } else { | ||
125 | expect(account.userId).to.be.undefined | ||
126 | } | ||
127 | |||
128 | for (const avatar of account.avatars) { | ||
129 | await testImage(server.url, `avatar2-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
130 | } | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | it('Should list account videos', async function () { | ||
135 | for (const server of servers) { | ||
136 | const { total, data } = await server.videos.listByAccount({ handle: 'user1@' + servers[0].host }) | ||
137 | |||
138 | expect(total).to.equal(1) | ||
139 | expect(data).to.be.an('array') | ||
140 | expect(data).to.have.lengthOf(1) | ||
141 | expect(data[0].uuid).to.equal(videoUUID) | ||
142 | } | ||
143 | }) | ||
144 | |||
145 | it('Should search through account videos', async function () { | ||
146 | const created = await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'Kami no chikara' } }) | ||
147 | |||
148 | await waitJobs(servers) | ||
149 | |||
150 | for (const server of servers) { | ||
151 | const { total, data } = await server.videos.listByAccount({ handle: 'user1@' + servers[0].host, search: 'Kami' }) | ||
152 | |||
153 | expect(total).to.equal(1) | ||
154 | expect(data).to.be.an('array') | ||
155 | expect(data).to.have.lengthOf(1) | ||
156 | expect(data[0].uuid).to.equal(created.uuid) | ||
157 | } | ||
158 | }) | ||
159 | |||
160 | it('Should remove the user', async function () { | ||
161 | this.timeout(10_000) | ||
162 | |||
163 | for (const server of servers) { | ||
164 | const body = await server.accounts.list({ sort: '-createdAt' }) | ||
165 | |||
166 | const accountDeleted = body.data.find(a => a.name === 'user1' && a.host === servers[0].host) | ||
167 | expect(accountDeleted).not.to.be.undefined | ||
168 | |||
169 | const { data } = await server.channels.list() | ||
170 | const videoChannelDeleted = data.find(a => a.displayName === 'Main user1 channel' && a.host === servers[0].host) | ||
171 | expect(videoChannelDeleted).not.to.be.undefined | ||
172 | } | ||
173 | |||
174 | await servers[0].users.remove({ userId }) | ||
175 | |||
176 | await waitJobs(servers) | ||
177 | |||
178 | for (const server of servers) { | ||
179 | const body = await server.accounts.list({ sort: '-createdAt' }) | ||
180 | |||
181 | const accountDeleted = body.data.find(a => a.name === 'user1' && a.host === servers[0].host) | ||
182 | expect(accountDeleted).to.be.undefined | ||
183 | |||
184 | const { data } = await server.channels.list() | ||
185 | const videoChannelDeleted = data.find(a => a.name === 'Main user1 channel' && a.host === servers[0].host) | ||
186 | expect(videoChannelDeleted).to.be.undefined | ||
187 | } | ||
188 | }) | ||
189 | |||
190 | it('Should not have actor files', async () => { | ||
191 | for (const server of servers) { | ||
192 | for (const userAvatarFilename of userAvatarFilenames) { | ||
193 | await checkActorFilesWereRemoved(userAvatarFilename, server) | ||
194 | } | ||
195 | } | ||
196 | }) | ||
197 | |||
198 | it('Should not have video files', async () => { | ||
199 | for (const server of servers) { | ||
200 | await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails }) | ||
201 | } | ||
202 | }) | ||
203 | |||
204 | it('Should have an empty tmp directory', async function () { | ||
205 | for (const server of servers) { | ||
206 | await checkTmpIsEmpty(server) | ||
207 | } | ||
208 | }) | ||
209 | |||
210 | after(async function () { | ||
211 | await cleanupTests(servers) | ||
212 | }) | ||
213 | }) | ||
diff --git a/packages/tests/src/api/users/users.ts b/packages/tests/src/api/users/users.ts new file mode 100644 index 000000000..a0090a463 --- /dev/null +++ b/packages/tests/src/api/users/users.ts | |||
@@ -0,0 +1,529 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { testImageSize } from '@tests/shared/checks.js' | ||
5 | import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@peertube/peertube-models' | ||
6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
7 | |||
8 | describe('Test users', function () { | ||
9 | let server: PeerTubeServer | ||
10 | let token: string | ||
11 | let userToken: string | ||
12 | let videoId: number | ||
13 | let userId: number | ||
14 | const user = { | ||
15 | username: 'user_1', | ||
16 | password: 'super password' | ||
17 | } | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(30000) | ||
21 | |||
22 | server = await createSingleServer(1, { | ||
23 | rates_limit: { | ||
24 | login: { | ||
25 | max: 30 | ||
26 | } | ||
27 | } | ||
28 | }) | ||
29 | |||
30 | await setAccessTokensToServers([ server ]) | ||
31 | |||
32 | await server.plugins.install({ npmName: 'peertube-theme-background-red' }) | ||
33 | }) | ||
34 | |||
35 | describe('Creating a user', function () { | ||
36 | |||
37 | it('Should be able to create a new user', async function () { | ||
38 | await server.users.create({ ...user, videoQuota: 2 * 1024 * 1024, adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST }) | ||
39 | }) | ||
40 | |||
41 | it('Should be able to login with this user', async function () { | ||
42 | userToken = await server.login.getAccessToken(user) | ||
43 | }) | ||
44 | |||
45 | it('Should be able to get user information', async function () { | ||
46 | const userMe = await server.users.getMyInfo({ token: userToken }) | ||
47 | |||
48 | const userGet = await server.users.get({ userId: userMe.id, withStats: true }) | ||
49 | |||
50 | for (const user of [ userMe, userGet ]) { | ||
51 | expect(user.username).to.equal('user_1') | ||
52 | expect(user.email).to.equal('user_1@example.com') | ||
53 | expect(user.nsfwPolicy).to.equal('display') | ||
54 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | ||
55 | expect(user.role.label).to.equal('User') | ||
56 | expect(user.id).to.be.a('number') | ||
57 | expect(user.account.displayName).to.equal('user_1') | ||
58 | expect(user.account.description).to.be.null | ||
59 | } | ||
60 | |||
61 | expect(userMe.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) | ||
62 | expect(userGet.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) | ||
63 | |||
64 | expect(userMe.specialPlaylists).to.have.lengthOf(1) | ||
65 | expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER) | ||
66 | |||
67 | // Check stats are included with withStats | ||
68 | expect(userGet.videosCount).to.be.a('number') | ||
69 | expect(userGet.videosCount).to.equal(0) | ||
70 | expect(userGet.videoCommentsCount).to.be.a('number') | ||
71 | expect(userGet.videoCommentsCount).to.equal(0) | ||
72 | expect(userGet.abusesCount).to.be.a('number') | ||
73 | expect(userGet.abusesCount).to.equal(0) | ||
74 | expect(userGet.abusesAcceptedCount).to.be.a('number') | ||
75 | expect(userGet.abusesAcceptedCount).to.equal(0) | ||
76 | }) | ||
77 | }) | ||
78 | |||
79 | describe('Users listing', function () { | ||
80 | |||
81 | it('Should list all the users', async function () { | ||
82 | const { data, total } = await server.users.list() | ||
83 | |||
84 | expect(total).to.equal(2) | ||
85 | expect(data).to.be.an('array') | ||
86 | expect(data.length).to.equal(2) | ||
87 | |||
88 | const user = data[0] | ||
89 | expect(user.username).to.equal('user_1') | ||
90 | expect(user.email).to.equal('user_1@example.com') | ||
91 | expect(user.nsfwPolicy).to.equal('display') | ||
92 | |||
93 | const rootUser = data[1] | ||
94 | expect(rootUser.username).to.equal('root') | ||
95 | expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com') | ||
96 | expect(user.nsfwPolicy).to.equal('display') | ||
97 | |||
98 | expect(rootUser.lastLoginDate).to.exist | ||
99 | expect(user.lastLoginDate).to.exist | ||
100 | |||
101 | userId = user.id | ||
102 | }) | ||
103 | |||
104 | it('Should list only the first user by username asc', async function () { | ||
105 | const { total, data } = await server.users.list({ start: 0, count: 1, sort: 'username' }) | ||
106 | |||
107 | expect(total).to.equal(2) | ||
108 | expect(data.length).to.equal(1) | ||
109 | |||
110 | const user = data[0] | ||
111 | expect(user.username).to.equal('root') | ||
112 | expect(user.email).to.equal('admin' + server.internalServerNumber + '@example.com') | ||
113 | expect(user.role.label).to.equal('Administrator') | ||
114 | expect(user.nsfwPolicy).to.equal('display') | ||
115 | }) | ||
116 | |||
117 | it('Should list only the first user by username desc', async function () { | ||
118 | const { total, data } = await server.users.list({ start: 0, count: 1, sort: '-username' }) | ||
119 | |||
120 | expect(total).to.equal(2) | ||
121 | expect(data.length).to.equal(1) | ||
122 | |||
123 | const user = data[0] | ||
124 | expect(user.username).to.equal('user_1') | ||
125 | expect(user.email).to.equal('user_1@example.com') | ||
126 | expect(user.nsfwPolicy).to.equal('display') | ||
127 | }) | ||
128 | |||
129 | it('Should list only the second user by createdAt desc', async function () { | ||
130 | const { data, total } = await server.users.list({ start: 0, count: 1, sort: '-createdAt' }) | ||
131 | expect(total).to.equal(2) | ||
132 | |||
133 | expect(data.length).to.equal(1) | ||
134 | |||
135 | const user = data[0] | ||
136 | expect(user.username).to.equal('user_1') | ||
137 | expect(user.email).to.equal('user_1@example.com') | ||
138 | expect(user.nsfwPolicy).to.equal('display') | ||
139 | }) | ||
140 | |||
141 | it('Should list all the users by createdAt asc', async function () { | ||
142 | const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt' }) | ||
143 | |||
144 | expect(total).to.equal(2) | ||
145 | expect(data.length).to.equal(2) | ||
146 | |||
147 | expect(data[0].username).to.equal('root') | ||
148 | expect(data[0].email).to.equal('admin' + server.internalServerNumber + '@example.com') | ||
149 | expect(data[0].nsfwPolicy).to.equal('display') | ||
150 | |||
151 | expect(data[1].username).to.equal('user_1') | ||
152 | expect(data[1].email).to.equal('user_1@example.com') | ||
153 | expect(data[1].nsfwPolicy).to.equal('display') | ||
154 | }) | ||
155 | |||
156 | it('Should search user by username', async function () { | ||
157 | const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'oot' }) | ||
158 | expect(total).to.equal(1) | ||
159 | expect(data.length).to.equal(1) | ||
160 | expect(data[0].username).to.equal('root') | ||
161 | }) | ||
162 | |||
163 | it('Should search user by email', async function () { | ||
164 | { | ||
165 | const { total, data } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'r_1@exam' }) | ||
166 | expect(total).to.equal(1) | ||
167 | expect(data.length).to.equal(1) | ||
168 | expect(data[0].username).to.equal('user_1') | ||
169 | expect(data[0].email).to.equal('user_1@example.com') | ||
170 | } | ||
171 | |||
172 | { | ||
173 | const { total, data } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'example' }) | ||
174 | expect(total).to.equal(2) | ||
175 | expect(data.length).to.equal(2) | ||
176 | expect(data[0].username).to.equal('root') | ||
177 | expect(data[1].username).to.equal('user_1') | ||
178 | } | ||
179 | }) | ||
180 | }) | ||
181 | |||
182 | describe('Update my account', function () { | ||
183 | |||
184 | it('Should update my password', async function () { | ||
185 | await server.users.updateMe({ | ||
186 | token: userToken, | ||
187 | currentPassword: 'super password', | ||
188 | password: 'new password' | ||
189 | }) | ||
190 | user.password = 'new password' | ||
191 | |||
192 | await server.login.login({ user }) | ||
193 | }) | ||
194 | |||
195 | it('Should be able to change the NSFW display attribute', async function () { | ||
196 | await server.users.updateMe({ | ||
197 | token: userToken, | ||
198 | nsfwPolicy: 'do_not_list' | ||
199 | }) | ||
200 | |||
201 | const user = await server.users.getMyInfo({ token: userToken }) | ||
202 | expect(user.username).to.equal('user_1') | ||
203 | expect(user.email).to.equal('user_1@example.com') | ||
204 | expect(user.nsfwPolicy).to.equal('do_not_list') | ||
205 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | ||
206 | expect(user.id).to.be.a('number') | ||
207 | expect(user.account.displayName).to.equal('user_1') | ||
208 | expect(user.account.description).to.be.null | ||
209 | }) | ||
210 | |||
211 | it('Should be able to change the autoPlayVideo attribute', async function () { | ||
212 | await server.users.updateMe({ | ||
213 | token: userToken, | ||
214 | autoPlayVideo: false | ||
215 | }) | ||
216 | |||
217 | const user = await server.users.getMyInfo({ token: userToken }) | ||
218 | expect(user.autoPlayVideo).to.be.false | ||
219 | }) | ||
220 | |||
221 | it('Should be able to change the autoPlayNextVideo attribute', async function () { | ||
222 | await server.users.updateMe({ | ||
223 | token: userToken, | ||
224 | autoPlayNextVideo: true | ||
225 | }) | ||
226 | |||
227 | const user = await server.users.getMyInfo({ token: userToken }) | ||
228 | expect(user.autoPlayNextVideo).to.be.true | ||
229 | }) | ||
230 | |||
231 | it('Should be able to change the p2p attribute', async function () { | ||
232 | await server.users.updateMe({ | ||
233 | token: userToken, | ||
234 | p2pEnabled: true | ||
235 | }) | ||
236 | |||
237 | const user = await server.users.getMyInfo({ token: userToken }) | ||
238 | expect(user.p2pEnabled).to.be.true | ||
239 | }) | ||
240 | |||
241 | it('Should be able to change the email attribute', async function () { | ||
242 | await server.users.updateMe({ | ||
243 | token: userToken, | ||
244 | currentPassword: 'new password', | ||
245 | email: 'updated@example.com' | ||
246 | }) | ||
247 | |||
248 | const user = await server.users.getMyInfo({ token: userToken }) | ||
249 | expect(user.username).to.equal('user_1') | ||
250 | expect(user.email).to.equal('updated@example.com') | ||
251 | expect(user.nsfwPolicy).to.equal('do_not_list') | ||
252 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | ||
253 | expect(user.id).to.be.a('number') | ||
254 | expect(user.account.displayName).to.equal('user_1') | ||
255 | expect(user.account.description).to.be.null | ||
256 | }) | ||
257 | |||
258 | it('Should be able to update my avatar with a gif', async function () { | ||
259 | const fixture = 'avatar.gif' | ||
260 | |||
261 | await server.users.updateMyAvatar({ token: userToken, fixture }) | ||
262 | |||
263 | const user = await server.users.getMyInfo({ token: userToken }) | ||
264 | for (const avatar of user.account.avatars) { | ||
265 | await testImageSize(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.gif') | ||
266 | } | ||
267 | }) | ||
268 | |||
269 | it('Should be able to update my avatar with a gif, and then a png', async function () { | ||
270 | for (const extension of [ '.png', '.gif' ]) { | ||
271 | const fixture = 'avatar' + extension | ||
272 | |||
273 | await server.users.updateMyAvatar({ token: userToken, fixture }) | ||
274 | |||
275 | const user = await server.users.getMyInfo({ token: userToken }) | ||
276 | for (const avatar of user.account.avatars) { | ||
277 | await testImageSize(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, extension) | ||
278 | } | ||
279 | } | ||
280 | }) | ||
281 | |||
282 | it('Should be able to update my display name', async function () { | ||
283 | await server.users.updateMe({ token: userToken, displayName: 'new display name' }) | ||
284 | |||
285 | const user = await server.users.getMyInfo({ token: userToken }) | ||
286 | expect(user.username).to.equal('user_1') | ||
287 | expect(user.email).to.equal('updated@example.com') | ||
288 | expect(user.nsfwPolicy).to.equal('do_not_list') | ||
289 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | ||
290 | expect(user.id).to.be.a('number') | ||
291 | expect(user.account.displayName).to.equal('new display name') | ||
292 | expect(user.account.description).to.be.null | ||
293 | }) | ||
294 | |||
295 | it('Should be able to update my description', async function () { | ||
296 | await server.users.updateMe({ token: userToken, description: 'my super description updated' }) | ||
297 | |||
298 | const user = await server.users.getMyInfo({ token: userToken }) | ||
299 | expect(user.username).to.equal('user_1') | ||
300 | expect(user.email).to.equal('updated@example.com') | ||
301 | expect(user.nsfwPolicy).to.equal('do_not_list') | ||
302 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | ||
303 | expect(user.id).to.be.a('number') | ||
304 | expect(user.account.displayName).to.equal('new display name') | ||
305 | expect(user.account.description).to.equal('my super description updated') | ||
306 | expect(user.noWelcomeModal).to.be.false | ||
307 | expect(user.noInstanceConfigWarningModal).to.be.false | ||
308 | expect(user.noAccountSetupWarningModal).to.be.false | ||
309 | }) | ||
310 | |||
311 | it('Should be able to update my theme', async function () { | ||
312 | for (const theme of [ 'background-red', 'default', 'instance-default' ]) { | ||
313 | await server.users.updateMe({ token: userToken, theme }) | ||
314 | |||
315 | const user = await server.users.getMyInfo({ token: userToken }) | ||
316 | expect(user.theme).to.equal(theme) | ||
317 | } | ||
318 | }) | ||
319 | |||
320 | it('Should be able to update my modal preferences', async function () { | ||
321 | await server.users.updateMe({ | ||
322 | token: userToken, | ||
323 | noInstanceConfigWarningModal: true, | ||
324 | noWelcomeModal: true, | ||
325 | noAccountSetupWarningModal: true | ||
326 | }) | ||
327 | |||
328 | const user = await server.users.getMyInfo({ token: userToken }) | ||
329 | expect(user.noWelcomeModal).to.be.true | ||
330 | expect(user.noInstanceConfigWarningModal).to.be.true | ||
331 | expect(user.noAccountSetupWarningModal).to.be.true | ||
332 | }) | ||
333 | }) | ||
334 | |||
335 | describe('Updating another user', function () { | ||
336 | |||
337 | it('Should be able to update another user', async function () { | ||
338 | await server.users.update({ | ||
339 | userId, | ||
340 | token, | ||
341 | email: 'updated2@example.com', | ||
342 | emailVerified: true, | ||
343 | videoQuota: 42, | ||
344 | role: UserRole.MODERATOR, | ||
345 | adminFlags: UserAdminFlag.NONE, | ||
346 | pluginAuth: 'toto' | ||
347 | }) | ||
348 | |||
349 | const user = await server.users.get({ token, userId }) | ||
350 | |||
351 | expect(user.username).to.equal('user_1') | ||
352 | expect(user.email).to.equal('updated2@example.com') | ||
353 | expect(user.emailVerified).to.be.true | ||
354 | expect(user.nsfwPolicy).to.equal('do_not_list') | ||
355 | expect(user.videoQuota).to.equal(42) | ||
356 | expect(user.role.label).to.equal('Moderator') | ||
357 | expect(user.id).to.be.a('number') | ||
358 | expect(user.adminFlags).to.equal(UserAdminFlag.NONE) | ||
359 | expect(user.pluginAuth).to.equal('toto') | ||
360 | }) | ||
361 | |||
362 | it('Should reset the auth plugin', async function () { | ||
363 | await server.users.update({ userId, token, pluginAuth: null }) | ||
364 | |||
365 | const user = await server.users.get({ token, userId }) | ||
366 | expect(user.pluginAuth).to.be.null | ||
367 | }) | ||
368 | |||
369 | it('Should have removed the user token', async function () { | ||
370 | await server.users.getMyQuotaUsed({ token: userToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
371 | |||
372 | userToken = await server.login.getAccessToken(user) | ||
373 | }) | ||
374 | |||
375 | it('Should be able to update another user password', async function () { | ||
376 | await server.users.update({ userId, token, password: 'password updated' }) | ||
377 | |||
378 | await server.users.getMyQuotaUsed({ token: userToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
379 | |||
380 | await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
381 | |||
382 | user.password = 'password updated' | ||
383 | userToken = await server.login.getAccessToken(user) | ||
384 | }) | ||
385 | }) | ||
386 | |||
387 | describe('Remove a user', function () { | ||
388 | |||
389 | before(async function () { | ||
390 | await server.users.update({ | ||
391 | userId, | ||
392 | token, | ||
393 | videoQuota: 2 * 1024 * 1024 | ||
394 | }) | ||
395 | |||
396 | await server.videos.quickUpload({ name: 'user video', token: userToken, fixture: 'video_short.webm' }) | ||
397 | await server.videos.quickUpload({ name: 'root video' }) | ||
398 | |||
399 | const { total } = await server.videos.list() | ||
400 | expect(total).to.equal(2) | ||
401 | }) | ||
402 | |||
403 | it('Should be able to remove this user', async function () { | ||
404 | await server.users.remove({ userId, token }) | ||
405 | }) | ||
406 | |||
407 | it('Should not be able to login with this user', async function () { | ||
408 | await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
409 | }) | ||
410 | |||
411 | it('Should not have videos of this user', async function () { | ||
412 | const { data, total } = await server.videos.list() | ||
413 | expect(total).to.equal(1) | ||
414 | |||
415 | const video = data[0] | ||
416 | expect(video.account.name).to.equal('root') | ||
417 | }) | ||
418 | }) | ||
419 | |||
420 | describe('User blocking', function () { | ||
421 | let user16Id: number | ||
422 | let user16AccessToken: string | ||
423 | |||
424 | const user16 = { | ||
425 | username: 'user_16', | ||
426 | password: 'my super password' | ||
427 | } | ||
428 | |||
429 | it('Should block a user', async function () { | ||
430 | const user = await server.users.create({ ...user16 }) | ||
431 | user16Id = user.id | ||
432 | |||
433 | user16AccessToken = await server.login.getAccessToken(user16) | ||
434 | |||
435 | await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
436 | await server.users.banUser({ userId: user16Id }) | ||
437 | |||
438 | await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
439 | await server.login.login({ user: user16, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
440 | }) | ||
441 | |||
442 | it('Should search user by banned status', async function () { | ||
443 | { | ||
444 | const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', blocked: true }) | ||
445 | expect(total).to.equal(1) | ||
446 | expect(data.length).to.equal(1) | ||
447 | |||
448 | expect(data[0].username).to.equal(user16.username) | ||
449 | } | ||
450 | |||
451 | { | ||
452 | const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', blocked: false }) | ||
453 | expect(total).to.equal(1) | ||
454 | expect(data.length).to.equal(1) | ||
455 | |||
456 | expect(data[0].username).to.not.equal(user16.username) | ||
457 | } | ||
458 | }) | ||
459 | |||
460 | it('Should unblock a user', async function () { | ||
461 | await server.users.unbanUser({ userId: user16Id }) | ||
462 | user16AccessToken = await server.login.getAccessToken(user16) | ||
463 | await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
464 | }) | ||
465 | }) | ||
466 | |||
467 | describe('User stats', function () { | ||
468 | let user17Id: number | ||
469 | let user17AccessToken: string | ||
470 | |||
471 | it('Should report correct initial statistics about a user', async function () { | ||
472 | const user17 = { | ||
473 | username: 'user_17', | ||
474 | password: 'my super password' | ||
475 | } | ||
476 | const created = await server.users.create({ ...user17 }) | ||
477 | |||
478 | user17Id = created.id | ||
479 | user17AccessToken = await server.login.getAccessToken(user17) | ||
480 | |||
481 | const user = await server.users.get({ userId: user17Id, withStats: true }) | ||
482 | expect(user.videosCount).to.equal(0) | ||
483 | expect(user.videoCommentsCount).to.equal(0) | ||
484 | expect(user.abusesCount).to.equal(0) | ||
485 | expect(user.abusesCreatedCount).to.equal(0) | ||
486 | expect(user.abusesAcceptedCount).to.equal(0) | ||
487 | }) | ||
488 | |||
489 | it('Should report correct videos count', async function () { | ||
490 | const attributes = { name: 'video to test user stats' } | ||
491 | await server.videos.upload({ token: user17AccessToken, attributes }) | ||
492 | |||
493 | const { data } = await server.videos.list() | ||
494 | videoId = data.find(video => video.name === attributes.name).id | ||
495 | |||
496 | const user = await server.users.get({ userId: user17Id, withStats: true }) | ||
497 | expect(user.videosCount).to.equal(1) | ||
498 | }) | ||
499 | |||
500 | it('Should report correct video comments for user', async function () { | ||
501 | const text = 'super comment' | ||
502 | await server.comments.createThread({ token: user17AccessToken, videoId, text }) | ||
503 | |||
504 | const user = await server.users.get({ userId: user17Id, withStats: true }) | ||
505 | expect(user.videoCommentsCount).to.equal(1) | ||
506 | }) | ||
507 | |||
508 | it('Should report correct abuses counts', async function () { | ||
509 | const reason = 'my super bad reason' | ||
510 | await server.abuses.report({ token: user17AccessToken, videoId, reason }) | ||
511 | |||
512 | const body1 = await server.abuses.getAdminList() | ||
513 | const abuseId = body1.data[0].id | ||
514 | |||
515 | const user2 = await server.users.get({ userId: user17Id, withStats: true }) | ||
516 | expect(user2.abusesCount).to.equal(1) // number of incriminations | ||
517 | expect(user2.abusesCreatedCount).to.equal(1) // number of reports created | ||
518 | |||
519 | await server.abuses.update({ abuseId, body: { state: AbuseState.ACCEPTED } }) | ||
520 | |||
521 | const user3 = await server.users.get({ userId: user17Id, withStats: true }) | ||
522 | expect(user3.abusesAcceptedCount).to.equal(1) // number of reports created accepted | ||
523 | }) | ||
524 | }) | ||
525 | |||
526 | after(async function () { | ||
527 | await cleanupTests([ server ]) | ||
528 | }) | ||
529 | }) | ||
diff --git a/packages/tests/src/api/videos/channel-import-videos.ts b/packages/tests/src/api/videos/channel-import-videos.ts new file mode 100644 index 000000000..d0e47fe95 --- /dev/null +++ b/packages/tests/src/api/videos/channel-import-videos.ts | |||
@@ -0,0 +1,161 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
5 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | createSingleServer, | ||
8 | getServerImportConfig, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultVideoChannel, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test videos import in a channel', function () { | ||
16 | if (areHttpImportTestsDisabled()) return | ||
17 | |||
18 | function runSuite (mode: 'youtube-dl' | 'yt-dlp') { | ||
19 | |||
20 | describe('Import using ' + mode, function () { | ||
21 | let server: PeerTubeServer | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(120_000) | ||
25 | |||
26 | server = await createSingleServer(1, getServerImportConfig(mode)) | ||
27 | |||
28 | await setAccessTokensToServers([ server ]) | ||
29 | await setDefaultVideoChannel([ server ]) | ||
30 | |||
31 | await server.config.enableChannelSync() | ||
32 | }) | ||
33 | |||
34 | it('Should import a whole channel without specifying the sync id', async function () { | ||
35 | this.timeout(240_000) | ||
36 | |||
37 | await server.channels.importVideos({ channelName: server.store.channel.name, externalChannelUrl: FIXTURE_URLS.youtubeChannel }) | ||
38 | await waitJobs(server) | ||
39 | |||
40 | const videos = await server.videos.listByChannel({ handle: server.store.channel.name }) | ||
41 | expect(videos.total).to.equal(2) | ||
42 | }) | ||
43 | |||
44 | it('These imports should not have a sync id', async function () { | ||
45 | const { total, data } = await server.imports.getMyVideoImports() | ||
46 | |||
47 | expect(total).to.equal(2) | ||
48 | expect(data).to.have.lengthOf(2) | ||
49 | |||
50 | for (const videoImport of data) { | ||
51 | expect(videoImport.videoChannelSync).to.not.exist | ||
52 | } | ||
53 | }) | ||
54 | |||
55 | it('Should import a whole channel and specifying the sync id', async function () { | ||
56 | this.timeout(240_000) | ||
57 | |||
58 | { | ||
59 | server.store.channel.name = 'channel2' | ||
60 | const { id } = await server.channels.create({ attributes: { name: server.store.channel.name } }) | ||
61 | server.store.channel.id = id | ||
62 | } | ||
63 | |||
64 | { | ||
65 | const attributes = { | ||
66 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
67 | videoChannelId: server.store.channel.id | ||
68 | } | ||
69 | |||
70 | const { videoChannelSync } = await server.channelSyncs.create({ attributes }) | ||
71 | server.store.videoChannelSync = videoChannelSync | ||
72 | |||
73 | await waitJobs(server) | ||
74 | } | ||
75 | |||
76 | await server.channels.importVideos({ | ||
77 | channelName: server.store.channel.name, | ||
78 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
79 | videoChannelSyncId: server.store.videoChannelSync.id | ||
80 | }) | ||
81 | |||
82 | await waitJobs(server) | ||
83 | }) | ||
84 | |||
85 | it('These imports should have a sync id', async function () { | ||
86 | const { total, data } = await server.imports.getMyVideoImports() | ||
87 | |||
88 | expect(total).to.equal(4) | ||
89 | expect(data).to.have.lengthOf(4) | ||
90 | |||
91 | const importsWithSyncId = data.filter(i => !!i.videoChannelSync) | ||
92 | expect(importsWithSyncId).to.have.lengthOf(2) | ||
93 | |||
94 | for (const videoImport of importsWithSyncId) { | ||
95 | expect(videoImport.videoChannelSync).to.exist | ||
96 | expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id) | ||
97 | } | ||
98 | }) | ||
99 | |||
100 | it('Should be able to filter imports by this sync id', async function () { | ||
101 | const { total, data } = await server.imports.getMyVideoImports({ videoChannelSyncId: server.store.videoChannelSync.id }) | ||
102 | |||
103 | expect(total).to.equal(2) | ||
104 | expect(data).to.have.lengthOf(2) | ||
105 | |||
106 | for (const videoImport of data) { | ||
107 | expect(videoImport.videoChannelSync).to.exist | ||
108 | expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id) | ||
109 | } | ||
110 | }) | ||
111 | |||
112 | it('Should limit max amount of videos synced on full sync', async function () { | ||
113 | this.timeout(240_000) | ||
114 | |||
115 | await server.kill() | ||
116 | await server.run({ | ||
117 | import: { | ||
118 | video_channel_synchronization: { | ||
119 | full_sync_videos_limit: 1 | ||
120 | } | ||
121 | } | ||
122 | }) | ||
123 | |||
124 | const { id } = await server.channels.create({ attributes: { name: 'channel3' } }) | ||
125 | const channel3Id = id | ||
126 | |||
127 | const { videoChannelSync } = await server.channelSyncs.create({ | ||
128 | attributes: { | ||
129 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
130 | videoChannelId: channel3Id | ||
131 | } | ||
132 | }) | ||
133 | const syncId = videoChannelSync.id | ||
134 | |||
135 | await waitJobs(server) | ||
136 | |||
137 | await server.channels.importVideos({ | ||
138 | channelName: 'channel3', | ||
139 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
140 | videoChannelSyncId: syncId | ||
141 | }) | ||
142 | |||
143 | await waitJobs(server) | ||
144 | |||
145 | const { total, data } = await server.videos.listByChannel({ handle: 'channel3' }) | ||
146 | |||
147 | expect(total).to.equal(1) | ||
148 | expect(data).to.have.lengthOf(1) | ||
149 | }) | ||
150 | |||
151 | after(async function () { | ||
152 | await server?.kill() | ||
153 | }) | ||
154 | }) | ||
155 | } | ||
156 | |||
157 | runSuite('yt-dlp') | ||
158 | |||
159 | // FIXME: With recent changes on youtube, youtube-dl doesn't fetch live replays which means the test suite fails | ||
160 | // runSuite('youtube-dl') | ||
161 | }) | ||
diff --git a/packages/tests/src/api/videos/index.ts b/packages/tests/src/api/videos/index.ts new file mode 100644 index 000000000..fcb1d5a81 --- /dev/null +++ b/packages/tests/src/api/videos/index.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | import './multiple-servers.js' | ||
2 | import './resumable-upload.js' | ||
3 | import './single-server.js' | ||
4 | import './video-captions.js' | ||
5 | import './video-change-ownership.js' | ||
6 | import './video-channels.js' | ||
7 | import './channel-import-videos.js' | ||
8 | import './video-channel-syncs.js' | ||
9 | import './video-comments.js' | ||
10 | import './video-description.js' | ||
11 | import './video-files.js' | ||
12 | import './video-imports.js' | ||
13 | import './video-nsfw.js' | ||
14 | import './video-playlists.js' | ||
15 | import './video-playlist-thumbnails.js' | ||
16 | import './video-source.js' | ||
17 | import './video-privacy.js' | ||
18 | import './video-schedule-update.js' | ||
19 | import './videos-common-filters.js' | ||
20 | import './videos-history.js' | ||
21 | import './videos-overview.js' | ||
22 | import './video-static-file-privacy.js' | ||
23 | import './video-storyboard.js' | ||
diff --git a/packages/tests/src/api/videos/multiple-servers.ts b/packages/tests/src/api/videos/multiple-servers.ts new file mode 100644 index 000000000..03afd7cbb --- /dev/null +++ b/packages/tests/src/api/videos/multiple-servers.ts | |||
@@ -0,0 +1,1095 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import request from 'supertest' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createMultipleServers, | ||
11 | doubleFollow, | ||
12 | makeGetRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultAccountAvatar, | ||
16 | setDefaultChannelAvatar, | ||
17 | waitJobs | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | import { testImageGeneratedByFFmpeg, dateIsValid } from '@tests/shared/checks.js' | ||
20 | import { checkTmpIsEmpty } from '@tests/shared/directories.js' | ||
21 | import { completeVideoCheck, saveVideoInServers, checkVideoFilesWereRemoved } from '@tests/shared/videos.js' | ||
22 | import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js' | ||
23 | |||
24 | describe('Test multiple servers', function () { | ||
25 | let servers: PeerTubeServer[] = [] | ||
26 | const toRemove = [] | ||
27 | let videoUUID = '' | ||
28 | let videoChannelId: number | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120000) | ||
32 | |||
33 | servers = await createMultipleServers(3) | ||
34 | |||
35 | // Get the access tokens | ||
36 | await setAccessTokensToServers(servers) | ||
37 | |||
38 | { | ||
39 | const videoChannel = { | ||
40 | name: 'super_channel_name', | ||
41 | displayName: 'my channel', | ||
42 | description: 'super channel' | ||
43 | } | ||
44 | await servers[0].channels.create({ attributes: videoChannel }) | ||
45 | await setDefaultChannelAvatar(servers[0], videoChannel.name) | ||
46 | await setDefaultAccountAvatar(servers) | ||
47 | |||
48 | const { data } = await servers[0].channels.list({ start: 0, count: 1 }) | ||
49 | videoChannelId = data[0].id | ||
50 | } | ||
51 | |||
52 | // Server 1 and server 2 follow each other | ||
53 | await doubleFollow(servers[0], servers[1]) | ||
54 | // Server 1 and server 3 follow each other | ||
55 | await doubleFollow(servers[0], servers[2]) | ||
56 | // Server 2 and server 3 follow each other | ||
57 | await doubleFollow(servers[1], servers[2]) | ||
58 | }) | ||
59 | |||
60 | it('Should not have videos for all servers', async function () { | ||
61 | for (const server of servers) { | ||
62 | const { data } = await server.videos.list() | ||
63 | expect(data).to.be.an('array') | ||
64 | expect(data.length).to.equal(0) | ||
65 | } | ||
66 | }) | ||
67 | |||
68 | describe('Should upload the video and propagate on each server', function () { | ||
69 | |||
70 | it('Should upload the video on server 1 and propagate on each server', async function () { | ||
71 | this.timeout(60000) | ||
72 | |||
73 | const attributes = { | ||
74 | name: 'my super name for server 1', | ||
75 | category: 5, | ||
76 | licence: 4, | ||
77 | language: 'ja', | ||
78 | nsfw: true, | ||
79 | description: 'my super description for server 1', | ||
80 | support: 'my super support text for server 1', | ||
81 | originallyPublishedAt: '2019-02-10T13:38:14.449Z', | ||
82 | tags: [ 'tag1p1', 'tag2p1' ], | ||
83 | channelId: videoChannelId, | ||
84 | fixture: 'video_short1.webm' | ||
85 | } | ||
86 | await servers[0].videos.upload({ attributes }) | ||
87 | |||
88 | await waitJobs(servers) | ||
89 | |||
90 | // All servers should have this video | ||
91 | let publishedAt: string = null | ||
92 | for (const server of servers) { | ||
93 | const isLocal = server.port === servers[0].port | ||
94 | const checkAttributes = { | ||
95 | name: 'my super name for server 1', | ||
96 | category: 5, | ||
97 | licence: 4, | ||
98 | language: 'ja', | ||
99 | nsfw: true, | ||
100 | description: 'my super description for server 1', | ||
101 | support: 'my super support text for server 1', | ||
102 | originallyPublishedAt: '2019-02-10T13:38:14.449Z', | ||
103 | account: { | ||
104 | name: 'root', | ||
105 | host: servers[0].host | ||
106 | }, | ||
107 | isLocal, | ||
108 | publishedAt, | ||
109 | duration: 10, | ||
110 | tags: [ 'tag1p1', 'tag2p1' ], | ||
111 | privacy: VideoPrivacy.PUBLIC, | ||
112 | commentsEnabled: true, | ||
113 | downloadEnabled: true, | ||
114 | channel: { | ||
115 | displayName: 'my channel', | ||
116 | name: 'super_channel_name', | ||
117 | description: 'super channel', | ||
118 | isLocal | ||
119 | }, | ||
120 | fixture: 'video_short1.webm', | ||
121 | files: [ | ||
122 | { | ||
123 | resolution: 720, | ||
124 | size: 572456 | ||
125 | } | ||
126 | ] | ||
127 | } | ||
128 | |||
129 | const { data } = await server.videos.list() | ||
130 | expect(data).to.be.an('array') | ||
131 | expect(data.length).to.equal(1) | ||
132 | const video = data[0] | ||
133 | |||
134 | await completeVideoCheck({ server, originServer: servers[0], videoUUID: video.uuid, attributes: checkAttributes }) | ||
135 | publishedAt = video.publishedAt as string | ||
136 | |||
137 | expect(video.channel.avatars).to.have.lengthOf(2) | ||
138 | expect(video.account.avatars).to.have.lengthOf(2) | ||
139 | |||
140 | for (const image of [ ...video.channel.avatars, ...video.account.avatars ]) { | ||
141 | expect(image.createdAt).to.exist | ||
142 | expect(image.updatedAt).to.exist | ||
143 | expect(image.width).to.be.above(20).and.below(1000) | ||
144 | expect(image.path).to.exist | ||
145 | |||
146 | await makeGetRequest({ | ||
147 | url: server.url, | ||
148 | path: image.path, | ||
149 | expectedStatus: HttpStatusCode.OK_200 | ||
150 | }) | ||
151 | } | ||
152 | } | ||
153 | }) | ||
154 | |||
155 | it('Should upload the video on server 2 and propagate on each server', async function () { | ||
156 | this.timeout(240000) | ||
157 | |||
158 | const user = { | ||
159 | username: 'user1', | ||
160 | password: 'super_password' | ||
161 | } | ||
162 | await servers[1].users.create({ username: user.username, password: user.password }) | ||
163 | const userAccessToken = await servers[1].login.getAccessToken(user) | ||
164 | |||
165 | const attributes = { | ||
166 | name: 'my super name for server 2', | ||
167 | category: 4, | ||
168 | licence: 3, | ||
169 | language: 'de', | ||
170 | nsfw: true, | ||
171 | description: 'my super description for server 2', | ||
172 | support: 'my super support text for server 2', | ||
173 | tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], | ||
174 | fixture: 'video_short2.webm', | ||
175 | thumbnailfile: 'custom-thumbnail.jpg', | ||
176 | previewfile: 'custom-preview.jpg' | ||
177 | } | ||
178 | await servers[1].videos.upload({ token: userAccessToken, attributes, mode: 'resumable' }) | ||
179 | |||
180 | // Transcoding | ||
181 | await waitJobs(servers) | ||
182 | |||
183 | // All servers should have this video | ||
184 | for (const server of servers) { | ||
185 | const isLocal = server.url === servers[1].url | ||
186 | const checkAttributes = { | ||
187 | name: 'my super name for server 2', | ||
188 | category: 4, | ||
189 | licence: 3, | ||
190 | language: 'de', | ||
191 | nsfw: true, | ||
192 | description: 'my super description for server 2', | ||
193 | support: 'my super support text for server 2', | ||
194 | account: { | ||
195 | name: 'user1', | ||
196 | host: servers[1].host | ||
197 | }, | ||
198 | isLocal, | ||
199 | commentsEnabled: true, | ||
200 | downloadEnabled: true, | ||
201 | duration: 5, | ||
202 | tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], | ||
203 | privacy: VideoPrivacy.PUBLIC, | ||
204 | channel: { | ||
205 | displayName: 'Main user1 channel', | ||
206 | name: 'user1_channel', | ||
207 | description: 'super channel', | ||
208 | isLocal | ||
209 | }, | ||
210 | fixture: 'video_short2.webm', | ||
211 | files: [ | ||
212 | { | ||
213 | resolution: 240, | ||
214 | size: 270000 | ||
215 | }, | ||
216 | { | ||
217 | resolution: 360, | ||
218 | size: 359000 | ||
219 | }, | ||
220 | { | ||
221 | resolution: 480, | ||
222 | size: 465000 | ||
223 | }, | ||
224 | { | ||
225 | resolution: 720, | ||
226 | size: 750000 | ||
227 | } | ||
228 | ], | ||
229 | thumbnailfile: 'custom-thumbnail', | ||
230 | previewfile: 'custom-preview' | ||
231 | } | ||
232 | |||
233 | const { data } = await server.videos.list() | ||
234 | expect(data).to.be.an('array') | ||
235 | expect(data.length).to.equal(2) | ||
236 | const video = data[1] | ||
237 | |||
238 | await completeVideoCheck({ server, originServer: servers[1], videoUUID: video.uuid, attributes: checkAttributes }) | ||
239 | } | ||
240 | }) | ||
241 | |||
242 | it('Should upload two videos on server 3 and propagate on each server', async function () { | ||
243 | this.timeout(45000) | ||
244 | |||
245 | { | ||
246 | const attributes = { | ||
247 | name: 'my super name for server 3', | ||
248 | category: 6, | ||
249 | licence: 5, | ||
250 | language: 'de', | ||
251 | nsfw: true, | ||
252 | description: 'my super description for server 3', | ||
253 | support: 'my super support text for server 3', | ||
254 | tags: [ 'tag1p3' ], | ||
255 | fixture: 'video_short3.webm' | ||
256 | } | ||
257 | await servers[2].videos.upload({ attributes }) | ||
258 | } | ||
259 | |||
260 | { | ||
261 | const attributes = { | ||
262 | name: 'my super name for server 3-2', | ||
263 | category: 7, | ||
264 | licence: 6, | ||
265 | language: 'ko', | ||
266 | nsfw: false, | ||
267 | description: 'my super description for server 3-2', | ||
268 | support: 'my super support text for server 3-2', | ||
269 | tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ], | ||
270 | fixture: 'video_short.webm' | ||
271 | } | ||
272 | await servers[2].videos.upload({ attributes }) | ||
273 | } | ||
274 | |||
275 | await waitJobs(servers) | ||
276 | |||
277 | // All servers should have this video | ||
278 | for (const server of servers) { | ||
279 | const isLocal = server.url === servers[2].url | ||
280 | const { data } = await server.videos.list() | ||
281 | |||
282 | expect(data).to.be.an('array') | ||
283 | expect(data.length).to.equal(4) | ||
284 | |||
285 | // We not sure about the order of the two last uploads | ||
286 | let video1 = null | ||
287 | let video2 = null | ||
288 | if (data[2].name === 'my super name for server 3') { | ||
289 | video1 = data[2] | ||
290 | video2 = data[3] | ||
291 | } else { | ||
292 | video1 = data[3] | ||
293 | video2 = data[2] | ||
294 | } | ||
295 | |||
296 | const checkAttributesVideo1 = { | ||
297 | name: 'my super name for server 3', | ||
298 | category: 6, | ||
299 | licence: 5, | ||
300 | language: 'de', | ||
301 | nsfw: true, | ||
302 | description: 'my super description for server 3', | ||
303 | support: 'my super support text for server 3', | ||
304 | account: { | ||
305 | name: 'root', | ||
306 | host: servers[2].host | ||
307 | }, | ||
308 | isLocal, | ||
309 | duration: 5, | ||
310 | commentsEnabled: true, | ||
311 | downloadEnabled: true, | ||
312 | tags: [ 'tag1p3' ], | ||
313 | privacy: VideoPrivacy.PUBLIC, | ||
314 | channel: { | ||
315 | displayName: 'Main root channel', | ||
316 | name: 'root_channel', | ||
317 | description: '', | ||
318 | isLocal | ||
319 | }, | ||
320 | fixture: 'video_short3.webm', | ||
321 | files: [ | ||
322 | { | ||
323 | resolution: 720, | ||
324 | size: 292677 | ||
325 | } | ||
326 | ] | ||
327 | } | ||
328 | await completeVideoCheck({ server, originServer: servers[2], videoUUID: video1.uuid, attributes: checkAttributesVideo1 }) | ||
329 | |||
330 | const checkAttributesVideo2 = { | ||
331 | name: 'my super name for server 3-2', | ||
332 | category: 7, | ||
333 | licence: 6, | ||
334 | language: 'ko', | ||
335 | nsfw: false, | ||
336 | description: 'my super description for server 3-2', | ||
337 | support: 'my super support text for server 3-2', | ||
338 | account: { | ||
339 | name: 'root', | ||
340 | host: servers[2].host | ||
341 | }, | ||
342 | commentsEnabled: true, | ||
343 | downloadEnabled: true, | ||
344 | isLocal, | ||
345 | duration: 5, | ||
346 | tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ], | ||
347 | privacy: VideoPrivacy.PUBLIC, | ||
348 | channel: { | ||
349 | displayName: 'Main root channel', | ||
350 | name: 'root_channel', | ||
351 | description: '', | ||
352 | isLocal | ||
353 | }, | ||
354 | fixture: 'video_short.webm', | ||
355 | files: [ | ||
356 | { | ||
357 | resolution: 720, | ||
358 | size: 218910 | ||
359 | } | ||
360 | ] | ||
361 | } | ||
362 | await completeVideoCheck({ server, originServer: servers[2], videoUUID: video2.uuid, attributes: checkAttributesVideo2 }) | ||
363 | } | ||
364 | }) | ||
365 | }) | ||
366 | |||
367 | describe('It should list local videos', function () { | ||
368 | it('Should list only local videos on server 1', async function () { | ||
369 | const { data, total } = await servers[0].videos.list({ isLocal: true }) | ||
370 | |||
371 | expect(total).to.equal(1) | ||
372 | expect(data).to.be.an('array') | ||
373 | expect(data.length).to.equal(1) | ||
374 | expect(data[0].name).to.equal('my super name for server 1') | ||
375 | }) | ||
376 | |||
377 | it('Should list only local videos on server 2', async function () { | ||
378 | const { data, total } = await servers[1].videos.list({ isLocal: true }) | ||
379 | |||
380 | expect(total).to.equal(1) | ||
381 | expect(data).to.be.an('array') | ||
382 | expect(data.length).to.equal(1) | ||
383 | expect(data[0].name).to.equal('my super name for server 2') | ||
384 | }) | ||
385 | |||
386 | it('Should list only local videos on server 3', async function () { | ||
387 | const { data, total } = await servers[2].videos.list({ isLocal: true }) | ||
388 | |||
389 | expect(total).to.equal(2) | ||
390 | expect(data).to.be.an('array') | ||
391 | expect(data.length).to.equal(2) | ||
392 | expect(data[0].name).to.equal('my super name for server 3') | ||
393 | expect(data[1].name).to.equal('my super name for server 3-2') | ||
394 | }) | ||
395 | }) | ||
396 | |||
397 | describe('Should seed the uploaded video', function () { | ||
398 | |||
399 | it('Should add the file 1 by asking server 3', async function () { | ||
400 | this.retries(2) | ||
401 | this.timeout(30000) | ||
402 | |||
403 | const { data } = await servers[2].videos.list() | ||
404 | |||
405 | const video = data[0] | ||
406 | toRemove.push(data[2]) | ||
407 | toRemove.push(data[3]) | ||
408 | |||
409 | const videoDetails = await servers[2].videos.get({ id: video.id }) | ||
410 | |||
411 | await checkWebTorrentWorks(videoDetails.files[0].magnetUri) | ||
412 | }) | ||
413 | |||
414 | it('Should add the file 2 by asking server 1', async function () { | ||
415 | this.retries(2) | ||
416 | this.timeout(30000) | ||
417 | |||
418 | const { data } = await servers[0].videos.list() | ||
419 | |||
420 | const video = data[1] | ||
421 | const videoDetails = await servers[0].videos.get({ id: video.id }) | ||
422 | |||
423 | await checkWebTorrentWorks(videoDetails.files[0].magnetUri) | ||
424 | }) | ||
425 | |||
426 | it('Should add the file 3 by asking server 2', async function () { | ||
427 | this.retries(2) | ||
428 | this.timeout(30000) | ||
429 | |||
430 | const { data } = await servers[1].videos.list() | ||
431 | |||
432 | const video = data[2] | ||
433 | const videoDetails = await servers[1].videos.get({ id: video.id }) | ||
434 | |||
435 | await checkWebTorrentWorks(videoDetails.files[0].magnetUri) | ||
436 | }) | ||
437 | |||
438 | it('Should add the file 3-2 by asking server 1', async function () { | ||
439 | this.retries(2) | ||
440 | this.timeout(30000) | ||
441 | |||
442 | const { data } = await servers[0].videos.list() | ||
443 | |||
444 | const video = data[3] | ||
445 | const videoDetails = await servers[0].videos.get({ id: video.id }) | ||
446 | |||
447 | await checkWebTorrentWorks(videoDetails.files[0].magnetUri) | ||
448 | }) | ||
449 | |||
450 | it('Should add the file 2 in 360p by asking server 1', async function () { | ||
451 | this.retries(2) | ||
452 | this.timeout(30000) | ||
453 | |||
454 | const { data } = await servers[0].videos.list() | ||
455 | |||
456 | const video = data.find(v => v.name === 'my super name for server 2') | ||
457 | const videoDetails = await servers[0].videos.get({ id: video.id }) | ||
458 | |||
459 | const file = videoDetails.files.find(f => f.resolution.id === 360) | ||
460 | expect(file).not.to.be.undefined | ||
461 | |||
462 | await checkWebTorrentWorks(file.magnetUri) | ||
463 | }) | ||
464 | }) | ||
465 | |||
466 | describe('Should update video views, likes and dislikes', function () { | ||
467 | let localVideosServer3 = [] | ||
468 | let remoteVideosServer1 = [] | ||
469 | let remoteVideosServer2 = [] | ||
470 | let remoteVideosServer3 = [] | ||
471 | |||
472 | before(async function () { | ||
473 | { | ||
474 | const { data } = await servers[0].videos.list() | ||
475 | remoteVideosServer1 = data.filter(video => video.isLocal === false).map(video => video.uuid) | ||
476 | } | ||
477 | |||
478 | { | ||
479 | const { data } = await servers[1].videos.list() | ||
480 | remoteVideosServer2 = data.filter(video => video.isLocal === false).map(video => video.uuid) | ||
481 | } | ||
482 | |||
483 | { | ||
484 | const { data } = await servers[2].videos.list() | ||
485 | localVideosServer3 = data.filter(video => video.isLocal === true).map(video => video.uuid) | ||
486 | remoteVideosServer3 = data.filter(video => video.isLocal === false).map(video => video.uuid) | ||
487 | } | ||
488 | }) | ||
489 | |||
490 | it('Should view multiple videos on owned servers', async function () { | ||
491 | this.timeout(30000) | ||
492 | |||
493 | await servers[2].views.simulateView({ id: localVideosServer3[0] }) | ||
494 | await wait(1000) | ||
495 | |||
496 | await servers[2].views.simulateView({ id: localVideosServer3[0] }) | ||
497 | await servers[2].views.simulateView({ id: localVideosServer3[1] }) | ||
498 | |||
499 | await wait(1000) | ||
500 | |||
501 | await servers[2].views.simulateView({ id: localVideosServer3[0] }) | ||
502 | await servers[2].views.simulateView({ id: localVideosServer3[0] }) | ||
503 | |||
504 | await waitJobs(servers) | ||
505 | |||
506 | for (const server of servers) { | ||
507 | await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } }) | ||
508 | } | ||
509 | |||
510 | await waitJobs(servers) | ||
511 | |||
512 | for (const server of servers) { | ||
513 | const { data } = await server.videos.list() | ||
514 | |||
515 | const video0 = data.find(v => v.uuid === localVideosServer3[0]) | ||
516 | const video1 = data.find(v => v.uuid === localVideosServer3[1]) | ||
517 | |||
518 | expect(video0.views).to.equal(3) | ||
519 | expect(video1.views).to.equal(1) | ||
520 | } | ||
521 | }) | ||
522 | |||
523 | it('Should view multiple videos on each servers', async function () { | ||
524 | this.timeout(45000) | ||
525 | |||
526 | const tasks: Promise<any>[] = [] | ||
527 | tasks.push(servers[0].views.simulateView({ id: remoteVideosServer1[0] })) | ||
528 | tasks.push(servers[1].views.simulateView({ id: remoteVideosServer2[0] })) | ||
529 | tasks.push(servers[1].views.simulateView({ id: remoteVideosServer2[0] })) | ||
530 | tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[0] })) | ||
531 | tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] })) | ||
532 | tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] })) | ||
533 | tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] })) | ||
534 | tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] })) | ||
535 | tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] })) | ||
536 | tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] })) | ||
537 | |||
538 | await Promise.all(tasks) | ||
539 | |||
540 | await waitJobs(servers) | ||
541 | |||
542 | for (const server of servers) { | ||
543 | await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } }) | ||
544 | } | ||
545 | |||
546 | await waitJobs(servers) | ||
547 | |||
548 | let baseVideos = null | ||
549 | |||
550 | for (const server of servers) { | ||
551 | const { data } = await server.videos.list() | ||
552 | |||
553 | // Initialize base videos for future comparisons | ||
554 | if (baseVideos === null) { | ||
555 | baseVideos = data | ||
556 | continue | ||
557 | } | ||
558 | |||
559 | for (const baseVideo of baseVideos) { | ||
560 | const sameVideo = data.find(video => video.name === baseVideo.name) | ||
561 | expect(baseVideo.views).to.equal(sameVideo.views) | ||
562 | } | ||
563 | } | ||
564 | }) | ||
565 | |||
566 | it('Should like and dislikes videos on different services', async function () { | ||
567 | this.timeout(50000) | ||
568 | |||
569 | await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'like' }) | ||
570 | await wait(500) | ||
571 | await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'dislike' }) | ||
572 | await wait(500) | ||
573 | await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'like' }) | ||
574 | await servers[2].videos.rate({ id: localVideosServer3[1], rating: 'like' }) | ||
575 | await wait(500) | ||
576 | await servers[2].videos.rate({ id: localVideosServer3[1], rating: 'dislike' }) | ||
577 | await servers[2].videos.rate({ id: remoteVideosServer3[1], rating: 'dislike' }) | ||
578 | await wait(500) | ||
579 | await servers[2].videos.rate({ id: remoteVideosServer3[0], rating: 'like' }) | ||
580 | |||
581 | await waitJobs(servers) | ||
582 | await wait(5000) | ||
583 | await waitJobs(servers) | ||
584 | |||
585 | let baseVideos = null | ||
586 | for (const server of servers) { | ||
587 | const { data } = await server.videos.list() | ||
588 | |||
589 | // Initialize base videos for future comparisons | ||
590 | if (baseVideos === null) { | ||
591 | baseVideos = data | ||
592 | continue | ||
593 | } | ||
594 | |||
595 | for (const baseVideo of baseVideos) { | ||
596 | const sameVideo = data.find(video => video.name === baseVideo.name) | ||
597 | expect(baseVideo.likes).to.equal(sameVideo.likes, `Likes of ${sameVideo.uuid} do not correspond`) | ||
598 | expect(baseVideo.dislikes).to.equal(sameVideo.dislikes, `Dislikes of ${sameVideo.uuid} do not correspond`) | ||
599 | } | ||
600 | } | ||
601 | }) | ||
602 | }) | ||
603 | |||
604 | describe('Should manipulate these videos', function () { | ||
605 | let updatedAtMin: Date | ||
606 | |||
607 | it('Should update video 3', async function () { | ||
608 | this.timeout(30000) | ||
609 | |||
610 | const attributes = { | ||
611 | name: 'my super video updated', | ||
612 | category: 10, | ||
613 | licence: 7, | ||
614 | language: 'fr', | ||
615 | nsfw: true, | ||
616 | description: 'my super description updated', | ||
617 | support: 'my super support text updated', | ||
618 | tags: [ 'tag_up_1', 'tag_up_2' ], | ||
619 | thumbnailfile: 'custom-thumbnail.jpg', | ||
620 | originallyPublishedAt: '2019-02-11T13:38:14.449Z', | ||
621 | previewfile: 'custom-preview.jpg' | ||
622 | } | ||
623 | |||
624 | updatedAtMin = new Date() | ||
625 | await servers[2].videos.update({ id: toRemove[0].id, attributes }) | ||
626 | |||
627 | await waitJobs(servers) | ||
628 | }) | ||
629 | |||
630 | it('Should have the video 3 updated on each server', async function () { | ||
631 | this.timeout(30000) | ||
632 | |||
633 | for (const server of servers) { | ||
634 | const { data } = await server.videos.list() | ||
635 | |||
636 | const videoUpdated = data.find(video => video.name === 'my super video updated') | ||
637 | expect(!!videoUpdated).to.be.true | ||
638 | |||
639 | expect(new Date(videoUpdated.updatedAt)).to.be.greaterThan(updatedAtMin) | ||
640 | |||
641 | const isLocal = server.url === servers[2].url | ||
642 | const checkAttributes = { | ||
643 | name: 'my super video updated', | ||
644 | category: 10, | ||
645 | licence: 7, | ||
646 | language: 'fr', | ||
647 | nsfw: true, | ||
648 | description: 'my super description updated', | ||
649 | support: 'my super support text updated', | ||
650 | originallyPublishedAt: '2019-02-11T13:38:14.449Z', | ||
651 | account: { | ||
652 | name: 'root', | ||
653 | host: servers[2].host | ||
654 | }, | ||
655 | isLocal, | ||
656 | duration: 5, | ||
657 | commentsEnabled: true, | ||
658 | downloadEnabled: true, | ||
659 | tags: [ 'tag_up_1', 'tag_up_2' ], | ||
660 | privacy: VideoPrivacy.PUBLIC, | ||
661 | channel: { | ||
662 | displayName: 'Main root channel', | ||
663 | name: 'root_channel', | ||
664 | description: '', | ||
665 | isLocal | ||
666 | }, | ||
667 | fixture: 'video_short3.webm', | ||
668 | files: [ | ||
669 | { | ||
670 | resolution: 720, | ||
671 | size: 292677 | ||
672 | } | ||
673 | ], | ||
674 | thumbnailfile: 'custom-thumbnail', | ||
675 | previewfile: 'custom-preview' | ||
676 | } | ||
677 | await completeVideoCheck({ server, originServer: servers[2], videoUUID: videoUpdated.uuid, attributes: checkAttributes }) | ||
678 | } | ||
679 | }) | ||
680 | |||
681 | it('Should only update thumbnail and update updatedAt attribute', async function () { | ||
682 | this.timeout(30000) | ||
683 | |||
684 | const attributes = { | ||
685 | thumbnailfile: 'custom-thumbnail.jpg' | ||
686 | } | ||
687 | |||
688 | updatedAtMin = new Date() | ||
689 | await servers[2].videos.update({ id: toRemove[0].id, attributes }) | ||
690 | |||
691 | await waitJobs(servers) | ||
692 | |||
693 | for (const server of servers) { | ||
694 | const { data } = await server.videos.list() | ||
695 | |||
696 | const videoUpdated = data.find(video => video.name === 'my super video updated') | ||
697 | expect(new Date(videoUpdated.updatedAt)).to.be.greaterThan(updatedAtMin) | ||
698 | } | ||
699 | }) | ||
700 | |||
701 | it('Should remove the videos 3 and 3-2 by asking server 3 and correctly delete files', async function () { | ||
702 | this.timeout(30000) | ||
703 | |||
704 | for (const id of [ toRemove[0].id, toRemove[1].id ]) { | ||
705 | await saveVideoInServers(servers, id) | ||
706 | |||
707 | await servers[2].videos.remove({ id }) | ||
708 | |||
709 | await waitJobs(servers) | ||
710 | |||
711 | for (const server of servers) { | ||
712 | await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails }) | ||
713 | } | ||
714 | } | ||
715 | }) | ||
716 | |||
717 | it('Should have videos 1 and 3 on each server', async function () { | ||
718 | for (const server of servers) { | ||
719 | const { data } = await server.videos.list() | ||
720 | |||
721 | expect(data).to.be.an('array') | ||
722 | expect(data.length).to.equal(2) | ||
723 | expect(data[0].name).not.to.equal(data[1].name) | ||
724 | expect(data[0].name).not.to.equal(toRemove[0].name) | ||
725 | expect(data[1].name).not.to.equal(toRemove[0].name) | ||
726 | expect(data[0].name).not.to.equal(toRemove[1].name) | ||
727 | expect(data[1].name).not.to.equal(toRemove[1].name) | ||
728 | |||
729 | videoUUID = data.find(video => video.name === 'my super name for server 1').uuid | ||
730 | } | ||
731 | }) | ||
732 | |||
733 | it('Should get the same video by UUID on each server', async function () { | ||
734 | let baseVideo = null | ||
735 | for (const server of servers) { | ||
736 | const video = await server.videos.get({ id: videoUUID }) | ||
737 | |||
738 | if (baseVideo === null) { | ||
739 | baseVideo = video | ||
740 | continue | ||
741 | } | ||
742 | |||
743 | expect(baseVideo.name).to.equal(video.name) | ||
744 | expect(baseVideo.uuid).to.equal(video.uuid) | ||
745 | expect(baseVideo.category.id).to.equal(video.category.id) | ||
746 | expect(baseVideo.language.id).to.equal(video.language.id) | ||
747 | expect(baseVideo.licence.id).to.equal(video.licence.id) | ||
748 | expect(baseVideo.nsfw).to.equal(video.nsfw) | ||
749 | expect(baseVideo.account.name).to.equal(video.account.name) | ||
750 | expect(baseVideo.account.displayName).to.equal(video.account.displayName) | ||
751 | expect(baseVideo.account.url).to.equal(video.account.url) | ||
752 | expect(baseVideo.account.host).to.equal(video.account.host) | ||
753 | expect(baseVideo.tags).to.deep.equal(video.tags) | ||
754 | } | ||
755 | }) | ||
756 | |||
757 | it('Should get the preview from each server', async function () { | ||
758 | for (const server of servers) { | ||
759 | const video = await server.videos.get({ id: videoUUID }) | ||
760 | |||
761 | await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath) | ||
762 | } | ||
763 | }) | ||
764 | }) | ||
765 | |||
766 | describe('Should comment these videos', function () { | ||
767 | let childOfFirstChild: VideoCommentThreadTree | ||
768 | |||
769 | it('Should add comment (threads and replies)', async function () { | ||
770 | this.timeout(25000) | ||
771 | |||
772 | { | ||
773 | const text = 'my super first comment' | ||
774 | await servers[0].comments.createThread({ videoId: videoUUID, text }) | ||
775 | } | ||
776 | |||
777 | { | ||
778 | const text = 'my super second comment' | ||
779 | await servers[2].comments.createThread({ videoId: videoUUID, text }) | ||
780 | } | ||
781 | |||
782 | await waitJobs(servers) | ||
783 | |||
784 | { | ||
785 | const threadId = await servers[1].comments.findCommentId({ videoId: videoUUID, text: 'my super first comment' }) | ||
786 | |||
787 | const text = 'my super answer to thread 1' | ||
788 | await servers[1].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text }) | ||
789 | } | ||
790 | |||
791 | await waitJobs(servers) | ||
792 | |||
793 | { | ||
794 | const threadId = await servers[2].comments.findCommentId({ videoId: videoUUID, text: 'my super first comment' }) | ||
795 | |||
796 | const body = await servers[2].comments.getThread({ videoId: videoUUID, threadId }) | ||
797 | const childCommentId = body.children[0].comment.id | ||
798 | |||
799 | const text3 = 'my second answer to thread 1' | ||
800 | await servers[2].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text: text3 }) | ||
801 | |||
802 | const text2 = 'my super answer to answer of thread 1' | ||
803 | await servers[2].comments.addReply({ videoId: videoUUID, toCommentId: childCommentId, text: text2 }) | ||
804 | } | ||
805 | |||
806 | await waitJobs(servers) | ||
807 | }) | ||
808 | |||
809 | it('Should have these threads', async function () { | ||
810 | for (const server of servers) { | ||
811 | const body = await server.comments.listThreads({ videoId: videoUUID }) | ||
812 | |||
813 | expect(body.total).to.equal(2) | ||
814 | expect(body.data).to.be.an('array') | ||
815 | expect(body.data).to.have.lengthOf(2) | ||
816 | |||
817 | { | ||
818 | const comment = body.data.find(c => c.text === 'my super first comment') | ||
819 | expect(comment).to.not.be.undefined | ||
820 | expect(comment.inReplyToCommentId).to.be.null | ||
821 | expect(comment.account.name).to.equal('root') | ||
822 | expect(comment.account.host).to.equal(servers[0].host) | ||
823 | expect(comment.totalReplies).to.equal(3) | ||
824 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
825 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
826 | } | ||
827 | |||
828 | { | ||
829 | const comment = body.data.find(c => c.text === 'my super second comment') | ||
830 | expect(comment).to.not.be.undefined | ||
831 | expect(comment.inReplyToCommentId).to.be.null | ||
832 | expect(comment.account.name).to.equal('root') | ||
833 | expect(comment.account.host).to.equal(servers[2].host) | ||
834 | expect(comment.totalReplies).to.equal(0) | ||
835 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
836 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
837 | } | ||
838 | } | ||
839 | }) | ||
840 | |||
841 | it('Should have these comments', async function () { | ||
842 | for (const server of servers) { | ||
843 | const body = await server.comments.listThreads({ videoId: videoUUID }) | ||
844 | const threadId = body.data.find(c => c.text === 'my super first comment').id | ||
845 | |||
846 | const tree = await server.comments.getThread({ videoId: videoUUID, threadId }) | ||
847 | |||
848 | expect(tree.comment.text).equal('my super first comment') | ||
849 | expect(tree.comment.account.name).equal('root') | ||
850 | expect(tree.comment.account.host).equal(servers[0].host) | ||
851 | expect(tree.children).to.have.lengthOf(2) | ||
852 | |||
853 | const firstChild = tree.children[0] | ||
854 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
855 | expect(firstChild.comment.account.name).equal('root') | ||
856 | expect(firstChild.comment.account.host).equal(servers[1].host) | ||
857 | expect(firstChild.children).to.have.lengthOf(1) | ||
858 | |||
859 | childOfFirstChild = firstChild.children[0] | ||
860 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
861 | expect(childOfFirstChild.comment.account.name).equal('root') | ||
862 | expect(childOfFirstChild.comment.account.host).equal(servers[2].host) | ||
863 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
864 | |||
865 | const secondChild = tree.children[1] | ||
866 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | ||
867 | expect(secondChild.comment.account.name).equal('root') | ||
868 | expect(secondChild.comment.account.host).equal(servers[2].host) | ||
869 | expect(secondChild.children).to.have.lengthOf(0) | ||
870 | } | ||
871 | }) | ||
872 | |||
873 | it('Should delete a reply', async function () { | ||
874 | this.timeout(30000) | ||
875 | |||
876 | await servers[2].comments.delete({ videoId: videoUUID, commentId: childOfFirstChild.comment.id }) | ||
877 | |||
878 | await waitJobs(servers) | ||
879 | }) | ||
880 | |||
881 | it('Should have this comment marked as deleted', async function () { | ||
882 | for (const server of servers) { | ||
883 | const { data } = await server.comments.listThreads({ videoId: videoUUID }) | ||
884 | const threadId = data.find(c => c.text === 'my super first comment').id | ||
885 | |||
886 | const tree = await server.comments.getThread({ videoId: videoUUID, threadId }) | ||
887 | expect(tree.comment.text).equal('my super first comment') | ||
888 | |||
889 | const firstChild = tree.children[0] | ||
890 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
891 | expect(firstChild.children).to.have.lengthOf(1) | ||
892 | |||
893 | const deletedComment = firstChild.children[0].comment | ||
894 | expect(deletedComment.isDeleted).to.be.true | ||
895 | expect(deletedComment.deletedAt).to.not.be.null | ||
896 | expect(deletedComment.account).to.be.null | ||
897 | expect(deletedComment.text).to.equal('') | ||
898 | |||
899 | const secondChild = tree.children[1] | ||
900 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | ||
901 | } | ||
902 | }) | ||
903 | |||
904 | it('Should delete the thread comments', async function () { | ||
905 | this.timeout(30000) | ||
906 | |||
907 | const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) | ||
908 | const commentId = data.find(c => c.text === 'my super first comment').id | ||
909 | await servers[0].comments.delete({ videoId: videoUUID, commentId }) | ||
910 | |||
911 | await waitJobs(servers) | ||
912 | }) | ||
913 | |||
914 | it('Should have the threads marked as deleted on other servers too', async function () { | ||
915 | for (const server of servers) { | ||
916 | const body = await server.comments.listThreads({ videoId: videoUUID }) | ||
917 | |||
918 | expect(body.total).to.equal(2) | ||
919 | expect(body.data).to.be.an('array') | ||
920 | expect(body.data).to.have.lengthOf(2) | ||
921 | |||
922 | { | ||
923 | const comment = body.data[0] | ||
924 | expect(comment).to.not.be.undefined | ||
925 | expect(comment.inReplyToCommentId).to.be.null | ||
926 | expect(comment.account.name).to.equal('root') | ||
927 | expect(comment.account.host).to.equal(servers[2].host) | ||
928 | expect(comment.totalReplies).to.equal(0) | ||
929 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
930 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
931 | } | ||
932 | |||
933 | { | ||
934 | const deletedComment = body.data[1] | ||
935 | expect(deletedComment).to.not.be.undefined | ||
936 | expect(deletedComment.isDeleted).to.be.true | ||
937 | expect(deletedComment.deletedAt).to.not.be.null | ||
938 | expect(deletedComment.text).to.equal('') | ||
939 | expect(deletedComment.inReplyToCommentId).to.be.null | ||
940 | expect(deletedComment.account).to.be.null | ||
941 | expect(deletedComment.totalReplies).to.equal(2) | ||
942 | expect(dateIsValid(deletedComment.createdAt as string)).to.be.true | ||
943 | expect(dateIsValid(deletedComment.updatedAt as string)).to.be.true | ||
944 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true | ||
945 | } | ||
946 | } | ||
947 | }) | ||
948 | |||
949 | it('Should delete a remote thread by the origin server', async function () { | ||
950 | this.timeout(5000) | ||
951 | |||
952 | const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) | ||
953 | const commentId = data.find(c => c.text === 'my super second comment').id | ||
954 | await servers[0].comments.delete({ videoId: videoUUID, commentId }) | ||
955 | |||
956 | await waitJobs(servers) | ||
957 | }) | ||
958 | |||
959 | it('Should have the threads marked as deleted on other servers too', async function () { | ||
960 | for (const server of servers) { | ||
961 | const body = await server.comments.listThreads({ videoId: videoUUID }) | ||
962 | |||
963 | expect(body.total).to.equal(2) | ||
964 | expect(body.data).to.have.lengthOf(2) | ||
965 | |||
966 | { | ||
967 | const comment = body.data[0] | ||
968 | expect(comment.text).to.equal('') | ||
969 | expect(comment.isDeleted).to.be.true | ||
970 | expect(comment.createdAt).to.not.be.null | ||
971 | expect(comment.deletedAt).to.not.be.null | ||
972 | expect(comment.account).to.be.null | ||
973 | expect(comment.totalReplies).to.equal(0) | ||
974 | } | ||
975 | |||
976 | { | ||
977 | const comment = body.data[1] | ||
978 | expect(comment.text).to.equal('') | ||
979 | expect(comment.isDeleted).to.be.true | ||
980 | expect(comment.createdAt).to.not.be.null | ||
981 | expect(comment.deletedAt).to.not.be.null | ||
982 | expect(comment.account).to.be.null | ||
983 | expect(comment.totalReplies).to.equal(2) | ||
984 | } | ||
985 | } | ||
986 | }) | ||
987 | |||
988 | it('Should disable comments and download', async function () { | ||
989 | this.timeout(20000) | ||
990 | |||
991 | const attributes = { | ||
992 | commentsEnabled: false, | ||
993 | downloadEnabled: false | ||
994 | } | ||
995 | |||
996 | await servers[0].videos.update({ id: videoUUID, attributes }) | ||
997 | |||
998 | await waitJobs(servers) | ||
999 | |||
1000 | for (const server of servers) { | ||
1001 | const video = await server.videos.get({ id: videoUUID }) | ||
1002 | expect(video.commentsEnabled).to.be.false | ||
1003 | expect(video.downloadEnabled).to.be.false | ||
1004 | |||
1005 | const text = 'my super forbidden comment' | ||
1006 | await server.comments.createThread({ videoId: videoUUID, text, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
1007 | } | ||
1008 | }) | ||
1009 | }) | ||
1010 | |||
1011 | describe('With minimum parameters', function () { | ||
1012 | it('Should upload and propagate the video', async function () { | ||
1013 | this.timeout(120000) | ||
1014 | |||
1015 | const path = '/api/v1/videos/upload' | ||
1016 | |||
1017 | const req = request(servers[1].url) | ||
1018 | .post(path) | ||
1019 | .set('Accept', 'application/json') | ||
1020 | .set('Authorization', 'Bearer ' + servers[1].accessToken) | ||
1021 | .field('name', 'minimum parameters') | ||
1022 | .field('privacy', '1') | ||
1023 | .field('channelId', '1') | ||
1024 | |||
1025 | await req.attach('videofile', buildAbsoluteFixturePath('video_short.webm')) | ||
1026 | .expect(HttpStatusCode.OK_200) | ||
1027 | |||
1028 | await waitJobs(servers) | ||
1029 | |||
1030 | for (const server of servers) { | ||
1031 | const { data } = await server.videos.list() | ||
1032 | const video = data.find(v => v.name === 'minimum parameters') | ||
1033 | |||
1034 | const isLocal = server.url === servers[1].url | ||
1035 | const checkAttributes = { | ||
1036 | name: 'minimum parameters', | ||
1037 | category: null, | ||
1038 | licence: null, | ||
1039 | language: null, | ||
1040 | nsfw: false, | ||
1041 | description: null, | ||
1042 | support: null, | ||
1043 | account: { | ||
1044 | name: 'root', | ||
1045 | host: servers[1].host | ||
1046 | }, | ||
1047 | isLocal, | ||
1048 | duration: 5, | ||
1049 | commentsEnabled: true, | ||
1050 | downloadEnabled: true, | ||
1051 | tags: [], | ||
1052 | privacy: VideoPrivacy.PUBLIC, | ||
1053 | channel: { | ||
1054 | displayName: 'Main root channel', | ||
1055 | name: 'root_channel', | ||
1056 | description: '', | ||
1057 | isLocal | ||
1058 | }, | ||
1059 | fixture: 'video_short.webm', | ||
1060 | files: [ | ||
1061 | { | ||
1062 | resolution: 720, | ||
1063 | size: 61000 | ||
1064 | }, | ||
1065 | { | ||
1066 | resolution: 480, | ||
1067 | size: 40000 | ||
1068 | }, | ||
1069 | { | ||
1070 | resolution: 360, | ||
1071 | size: 32000 | ||
1072 | }, | ||
1073 | { | ||
1074 | resolution: 240, | ||
1075 | size: 23000 | ||
1076 | } | ||
1077 | ] | ||
1078 | } | ||
1079 | await completeVideoCheck({ server, originServer: servers[1], videoUUID: video.uuid, attributes: checkAttributes }) | ||
1080 | } | ||
1081 | }) | ||
1082 | }) | ||
1083 | |||
1084 | describe('TMP directory', function () { | ||
1085 | it('Should have an empty tmp directory', async function () { | ||
1086 | for (const server of servers) { | ||
1087 | await checkTmpIsEmpty(server) | ||
1088 | } | ||
1089 | }) | ||
1090 | }) | ||
1091 | |||
1092 | after(async function () { | ||
1093 | await cleanupTests(servers) | ||
1094 | }) | ||
1095 | }) | ||
diff --git a/packages/tests/src/api/videos/resumable-upload.ts b/packages/tests/src/api/videos/resumable-upload.ts new file mode 100644 index 000000000..628e0298c --- /dev/null +++ b/packages/tests/src/api/videos/resumable-upload.ts | |||
@@ -0,0 +1,316 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { readdir, stat } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { HttpStatusCode, HttpStatusCodeType, VideoPrivacy } from '@peertube/peertube-models' | ||
8 | import { buildAbsoluteFixturePath, sha1 } from '@peertube/peertube-node-utils' | ||
9 | import { | ||
10 | cleanupTests, | ||
11 | createSingleServer, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | setDefaultVideoChannel | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | // Most classic resumable upload tests are done in other test suites | ||
18 | |||
19 | describe('Test resumable upload', function () { | ||
20 | const path = '/api/v1/videos/upload-resumable' | ||
21 | const defaultFixture = 'video_short.mp4' | ||
22 | let server: PeerTubeServer | ||
23 | let rootId: number | ||
24 | let userAccessToken: string | ||
25 | let userChannelId: number | ||
26 | |||
27 | async function buildSize (fixture: string, size?: number) { | ||
28 | if (size !== undefined) return size | ||
29 | |||
30 | const baseFixture = buildAbsoluteFixturePath(fixture) | ||
31 | return (await stat(baseFixture)).size | ||
32 | } | ||
33 | |||
34 | async function prepareUpload (options: { | ||
35 | channelId?: number | ||
36 | token?: string | ||
37 | size?: number | ||
38 | originalName?: string | ||
39 | lastModified?: number | ||
40 | } = {}) { | ||
41 | const { token, originalName, lastModified } = options | ||
42 | |||
43 | const size = await buildSize(defaultFixture, options.size) | ||
44 | |||
45 | const attributes = { | ||
46 | name: 'video', | ||
47 | channelId: options.channelId ?? server.store.channel.id, | ||
48 | privacy: VideoPrivacy.PUBLIC, | ||
49 | fixture: defaultFixture | ||
50 | } | ||
51 | |||
52 | const mimetype = 'video/mp4' | ||
53 | |||
54 | const res = await server.videos.prepareResumableUpload({ path, token, attributes, size, mimetype, originalName, lastModified }) | ||
55 | |||
56 | return res.header['location'].split('?')[1] | ||
57 | } | ||
58 | |||
59 | async function sendChunks (options: { | ||
60 | token?: string | ||
61 | pathUploadId: string | ||
62 | size?: number | ||
63 | expectedStatus?: HttpStatusCodeType | ||
64 | contentLength?: number | ||
65 | contentRange?: string | ||
66 | contentRangeBuilder?: (start: number, chunk: any) => string | ||
67 | digestBuilder?: (chunk: any) => string | ||
68 | }) { | ||
69 | const { token, pathUploadId, expectedStatus, contentLength, contentRangeBuilder, digestBuilder } = options | ||
70 | |||
71 | const size = await buildSize(defaultFixture, options.size) | ||
72 | const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture) | ||
73 | |||
74 | return server.videos.sendResumableChunks({ | ||
75 | token, | ||
76 | path, | ||
77 | pathUploadId, | ||
78 | videoFilePath: absoluteFilePath, | ||
79 | size, | ||
80 | contentLength, | ||
81 | contentRangeBuilder, | ||
82 | digestBuilder, | ||
83 | expectedStatus | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | async function checkFileSize (uploadIdArg: string, expectedSize: number | null) { | ||
88 | const uploadId = uploadIdArg.replace(/^upload_id=/, '') | ||
89 | |||
90 | const subPath = join('tmp', 'resumable-uploads', `${rootId}-${uploadId}.mp4`) | ||
91 | const filePath = server.servers.buildDirectory(subPath) | ||
92 | const exists = await pathExists(filePath) | ||
93 | |||
94 | if (expectedSize === null) { | ||
95 | expect(exists).to.be.false | ||
96 | return | ||
97 | } | ||
98 | |||
99 | expect(exists).to.be.true | ||
100 | |||
101 | expect((await stat(filePath)).size).to.equal(expectedSize) | ||
102 | } | ||
103 | |||
104 | async function countResumableUploads (wait?: number) { | ||
105 | const subPath = join('tmp', 'resumable-uploads') | ||
106 | const filePath = server.servers.buildDirectory(subPath) | ||
107 | await new Promise(resolve => setTimeout(resolve, wait)) | ||
108 | const files = await readdir(filePath) | ||
109 | return files.length | ||
110 | } | ||
111 | |||
112 | before(async function () { | ||
113 | this.timeout(30000) | ||
114 | |||
115 | server = await createSingleServer(1) | ||
116 | await setAccessTokensToServers([ server ]) | ||
117 | await setDefaultVideoChannel([ server ]) | ||
118 | |||
119 | const body = await server.users.getMyInfo() | ||
120 | rootId = body.id | ||
121 | |||
122 | { | ||
123 | userAccessToken = await server.users.generateUserAndToken('user1') | ||
124 | const { videoChannels } = await server.users.getMyInfo({ token: userAccessToken }) | ||
125 | userChannelId = videoChannels[0].id | ||
126 | } | ||
127 | |||
128 | await server.users.update({ userId: rootId, videoQuota: 10_000_000 }) | ||
129 | }) | ||
130 | |||
131 | describe('Directory cleaning', function () { | ||
132 | |||
133 | it('Should correctly delete files after an upload', async function () { | ||
134 | const uploadId = await prepareUpload() | ||
135 | await sendChunks({ pathUploadId: uploadId }) | ||
136 | await server.videos.endResumableUpload({ path, pathUploadId: uploadId }) | ||
137 | |||
138 | expect(await countResumableUploads()).to.equal(0) | ||
139 | }) | ||
140 | |||
141 | it('Should correctly delete corrupt files', async function () { | ||
142 | const uploadId = await prepareUpload({ size: 8 * 1024 }) | ||
143 | await sendChunks({ pathUploadId: uploadId, size: 8 * 1024, expectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 }) | ||
144 | |||
145 | expect(await countResumableUploads(2000)).to.equal(0) | ||
146 | }) | ||
147 | |||
148 | it('Should not delete files after an unfinished upload', async function () { | ||
149 | await prepareUpload() | ||
150 | |||
151 | expect(await countResumableUploads()).to.equal(2) | ||
152 | }) | ||
153 | |||
154 | it('Should not delete recent uploads', async function () { | ||
155 | await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } }) | ||
156 | |||
157 | expect(await countResumableUploads()).to.equal(2) | ||
158 | }) | ||
159 | |||
160 | it('Should delete old uploads', async function () { | ||
161 | await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } }) | ||
162 | |||
163 | expect(await countResumableUploads()).to.equal(0) | ||
164 | }) | ||
165 | }) | ||
166 | |||
167 | describe('Resumable upload and chunks', function () { | ||
168 | |||
169 | it('Should accept the same amount of chunks', async function () { | ||
170 | const uploadId = await prepareUpload() | ||
171 | await sendChunks({ pathUploadId: uploadId }) | ||
172 | |||
173 | await checkFileSize(uploadId, null) | ||
174 | }) | ||
175 | |||
176 | it('Should not accept more chunks than expected', async function () { | ||
177 | const uploadId = await prepareUpload({ size: 100 }) | ||
178 | |||
179 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
180 | await checkFileSize(uploadId, 0) | ||
181 | }) | ||
182 | |||
183 | it('Should not accept more chunks than expected with an invalid content length/content range', async function () { | ||
184 | const uploadId = await prepareUpload({ size: 1500 }) | ||
185 | |||
186 | // Content length check can be different depending on the node version | ||
187 | try { | ||
188 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409, contentLength: 1000 }) | ||
189 | await checkFileSize(uploadId, 0) | ||
190 | } catch { | ||
191 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 }) | ||
192 | await checkFileSize(uploadId, 0) | ||
193 | } | ||
194 | }) | ||
195 | |||
196 | it('Should not accept more chunks than expected with an invalid content length', async function () { | ||
197 | const uploadId = await prepareUpload({ size: 500 }) | ||
198 | |||
199 | const size = 1000 | ||
200 | |||
201 | // Content length check seems to have changed in v16 | ||
202 | const expectedStatus = process.version.startsWith('v16') | ||
203 | ? HttpStatusCode.CONFLICT_409 | ||
204 | : HttpStatusCode.BAD_REQUEST_400 | ||
205 | |||
206 | const contentRangeBuilder = (start: number) => `bytes ${start}-${start + size - 1}/${size}` | ||
207 | await sendChunks({ pathUploadId: uploadId, expectedStatus, contentRangeBuilder, contentLength: size }) | ||
208 | await checkFileSize(uploadId, 0) | ||
209 | }) | ||
210 | |||
211 | it('Should be able to accept 2 PUT requests', async function () { | ||
212 | const uploadId = await prepareUpload() | ||
213 | |||
214 | const result1 = await sendChunks({ pathUploadId: uploadId }) | ||
215 | const result2 = await sendChunks({ pathUploadId: uploadId }) | ||
216 | |||
217 | expect(result1.body.video.uuid).to.exist | ||
218 | expect(result1.body.video.uuid).to.equal(result2.body.video.uuid) | ||
219 | |||
220 | expect(result1.headers['x-resumable-upload-cached']).to.not.exist | ||
221 | expect(result2.headers['x-resumable-upload-cached']).to.equal('true') | ||
222 | |||
223 | await checkFileSize(uploadId, null) | ||
224 | }) | ||
225 | |||
226 | it('Should not have the same upload id with 2 different users', async function () { | ||
227 | const originalName = 'toto.mp4' | ||
228 | const lastModified = new Date().getTime() | ||
229 | |||
230 | const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken }) | ||
231 | const uploadId2 = await prepareUpload({ originalName, lastModified, channelId: userChannelId, token: userAccessToken }) | ||
232 | |||
233 | expect(uploadId1).to.not.equal(uploadId2) | ||
234 | }) | ||
235 | |||
236 | it('Should have the same upload id with the same user', async function () { | ||
237 | const originalName = 'toto.mp4' | ||
238 | const lastModified = new Date().getTime() | ||
239 | |||
240 | const uploadId1 = await prepareUpload({ originalName, lastModified }) | ||
241 | const uploadId2 = await prepareUpload({ originalName, lastModified }) | ||
242 | |||
243 | expect(uploadId1).to.equal(uploadId2) | ||
244 | }) | ||
245 | |||
246 | it('Should not cache a request with 2 different users', async function () { | ||
247 | const originalName = 'toto.mp4' | ||
248 | const lastModified = new Date().getTime() | ||
249 | |||
250 | const uploadId = await prepareUpload({ originalName, lastModified, token: server.accessToken }) | ||
251 | |||
252 | await sendChunks({ pathUploadId: uploadId, token: server.accessToken }) | ||
253 | await sendChunks({ pathUploadId: uploadId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
254 | }) | ||
255 | |||
256 | it('Should not cache a request after a delete', async function () { | ||
257 | const originalName = 'toto.mp4' | ||
258 | const lastModified = new Date().getTime() | ||
259 | const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken }) | ||
260 | |||
261 | await sendChunks({ pathUploadId: uploadId1 }) | ||
262 | await server.videos.endResumableUpload({ path, pathUploadId: uploadId1 }) | ||
263 | |||
264 | const uploadId2 = await prepareUpload({ originalName, lastModified, token: server.accessToken }) | ||
265 | expect(uploadId1).to.equal(uploadId2) | ||
266 | |||
267 | const result2 = await sendChunks({ pathUploadId: uploadId1 }) | ||
268 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist | ||
269 | }) | ||
270 | |||
271 | it('Should not cache after video deletion', async function () { | ||
272 | const originalName = 'toto.mp4' | ||
273 | const lastModified = new Date().getTime() | ||
274 | |||
275 | const uploadId1 = await prepareUpload({ originalName, lastModified }) | ||
276 | const result1 = await sendChunks({ pathUploadId: uploadId1 }) | ||
277 | await server.videos.remove({ id: result1.body.video.uuid }) | ||
278 | |||
279 | const uploadId2 = await prepareUpload({ originalName, lastModified }) | ||
280 | const result2 = await sendChunks({ pathUploadId: uploadId2 }) | ||
281 | expect(result1.body.video.uuid).to.not.equal(result2.body.video.uuid) | ||
282 | |||
283 | expect(result2.headers['x-resumable-upload-cached']).to.not.exist | ||
284 | |||
285 | await checkFileSize(uploadId1, null) | ||
286 | await checkFileSize(uploadId2, null) | ||
287 | }) | ||
288 | |||
289 | it('Should refuse an invalid digest', async function () { | ||
290 | const uploadId = await prepareUpload({ token: server.accessToken }) | ||
291 | |||
292 | await sendChunks({ | ||
293 | pathUploadId: uploadId, | ||
294 | token: server.accessToken, | ||
295 | digestBuilder: () => 'sha=' + 'a'.repeat(40), | ||
296 | expectedStatus: 460 as any | ||
297 | }) | ||
298 | }) | ||
299 | |||
300 | it('Should accept an appropriate digest', async function () { | ||
301 | const uploadId = await prepareUpload({ token: server.accessToken }) | ||
302 | |||
303 | await sendChunks({ | ||
304 | pathUploadId: uploadId, | ||
305 | token: server.accessToken, | ||
306 | digestBuilder: (chunk: Buffer) => { | ||
307 | return 'sha1=' + sha1(chunk, 'base64') | ||
308 | } | ||
309 | }) | ||
310 | }) | ||
311 | }) | ||
312 | |||
313 | after(async function () { | ||
314 | await cleanupTests([ server ]) | ||
315 | }) | ||
316 | }) | ||
diff --git a/packages/tests/src/api/videos/single-server.ts b/packages/tests/src/api/videos/single-server.ts new file mode 100644 index 000000000..b87192a57 --- /dev/null +++ b/packages/tests/src/api/videos/single-server.ts | |||
@@ -0,0 +1,461 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { Video, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { checkVideoFilesWereRemoved, completeVideoCheck } from '@tests/shared/videos.js' | ||
7 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar, | ||
15 | waitJobs | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('Test a single server', function () { | ||
19 | |||
20 | function runSuite (mode: 'legacy' | 'resumable') { | ||
21 | let server: PeerTubeServer = null | ||
22 | let videoId: number | string | ||
23 | let videoId2: string | ||
24 | let videoUUID = '' | ||
25 | let videosListBase: any[] = null | ||
26 | |||
27 | const getCheckAttributes = () => ({ | ||
28 | name: 'my super name', | ||
29 | category: 2, | ||
30 | licence: 6, | ||
31 | language: 'zh', | ||
32 | nsfw: true, | ||
33 | description: 'my super description', | ||
34 | support: 'my super support text', | ||
35 | account: { | ||
36 | name: 'root', | ||
37 | host: server.host | ||
38 | }, | ||
39 | isLocal: true, | ||
40 | duration: 5, | ||
41 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
42 | privacy: VideoPrivacy.PUBLIC, | ||
43 | commentsEnabled: true, | ||
44 | downloadEnabled: true, | ||
45 | channel: { | ||
46 | displayName: 'Main root channel', | ||
47 | name: 'root_channel', | ||
48 | description: '', | ||
49 | isLocal: true | ||
50 | }, | ||
51 | fixture: 'video_short.webm', | ||
52 | files: [ | ||
53 | { | ||
54 | resolution: 720, | ||
55 | size: 218910 | ||
56 | } | ||
57 | ] | ||
58 | }) | ||
59 | |||
60 | const updateCheckAttributes = () => ({ | ||
61 | name: 'my super video updated', | ||
62 | category: 4, | ||
63 | licence: 2, | ||
64 | language: 'ar', | ||
65 | nsfw: false, | ||
66 | description: 'my super description updated', | ||
67 | support: 'my super support text updated', | ||
68 | account: { | ||
69 | name: 'root', | ||
70 | host: server.host | ||
71 | }, | ||
72 | isLocal: true, | ||
73 | tags: [ 'tagup1', 'tagup2' ], | ||
74 | privacy: VideoPrivacy.PUBLIC, | ||
75 | duration: 5, | ||
76 | commentsEnabled: false, | ||
77 | downloadEnabled: false, | ||
78 | channel: { | ||
79 | name: 'root_channel', | ||
80 | displayName: 'Main root channel', | ||
81 | description: '', | ||
82 | isLocal: true | ||
83 | }, | ||
84 | fixture: 'video_short3.webm', | ||
85 | files: [ | ||
86 | { | ||
87 | resolution: 720, | ||
88 | size: 292677 | ||
89 | } | ||
90 | ] | ||
91 | }) | ||
92 | |||
93 | before(async function () { | ||
94 | this.timeout(30000) | ||
95 | |||
96 | server = await createSingleServer(1, {}) | ||
97 | |||
98 | await setAccessTokensToServers([ server ]) | ||
99 | await setDefaultChannelAvatar(server) | ||
100 | await setDefaultAccountAvatar(server) | ||
101 | }) | ||
102 | |||
103 | it('Should list video categories', async function () { | ||
104 | const categories = await server.videos.getCategories() | ||
105 | expect(Object.keys(categories)).to.have.length.above(10) | ||
106 | |||
107 | expect(categories[11]).to.equal('News & Politics') | ||
108 | }) | ||
109 | |||
110 | it('Should list video licences', async function () { | ||
111 | const licences = await server.videos.getLicences() | ||
112 | expect(Object.keys(licences)).to.have.length.above(5) | ||
113 | |||
114 | expect(licences[3]).to.equal('Attribution - No Derivatives') | ||
115 | }) | ||
116 | |||
117 | it('Should list video languages', async function () { | ||
118 | const languages = await server.videos.getLanguages() | ||
119 | expect(Object.keys(languages)).to.have.length.above(5) | ||
120 | |||
121 | expect(languages['ru']).to.equal('Russian') | ||
122 | }) | ||
123 | |||
124 | it('Should list video privacies', async function () { | ||
125 | const privacies = await server.videos.getPrivacies() | ||
126 | expect(Object.keys(privacies)).to.have.length.at.least(3) | ||
127 | |||
128 | expect(privacies[3]).to.equal('Private') | ||
129 | }) | ||
130 | |||
131 | it('Should not have videos', async function () { | ||
132 | const { data, total } = await server.videos.list() | ||
133 | |||
134 | expect(total).to.equal(0) | ||
135 | expect(data).to.be.an('array') | ||
136 | expect(data.length).to.equal(0) | ||
137 | }) | ||
138 | |||
139 | it('Should upload the video', async function () { | ||
140 | const attributes = { | ||
141 | name: 'my super name', | ||
142 | category: 2, | ||
143 | nsfw: true, | ||
144 | licence: 6, | ||
145 | tags: [ 'tag1', 'tag2', 'tag3' ] | ||
146 | } | ||
147 | const video = await server.videos.upload({ attributes, mode }) | ||
148 | expect(video).to.not.be.undefined | ||
149 | expect(video.id).to.equal(1) | ||
150 | expect(video.uuid).to.have.length.above(5) | ||
151 | |||
152 | videoId = video.id | ||
153 | videoUUID = video.uuid | ||
154 | }) | ||
155 | |||
156 | it('Should get and seed the uploaded video', async function () { | ||
157 | this.timeout(5000) | ||
158 | |||
159 | const { data, total } = await server.videos.list() | ||
160 | |||
161 | expect(total).to.equal(1) | ||
162 | expect(data).to.be.an('array') | ||
163 | expect(data.length).to.equal(1) | ||
164 | |||
165 | const video = data[0] | ||
166 | await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: getCheckAttributes() }) | ||
167 | }) | ||
168 | |||
169 | it('Should get the video by UUID', async function () { | ||
170 | this.timeout(5000) | ||
171 | |||
172 | const video = await server.videos.get({ id: videoUUID }) | ||
173 | await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: getCheckAttributes() }) | ||
174 | }) | ||
175 | |||
176 | it('Should have the views updated', async function () { | ||
177 | this.timeout(20000) | ||
178 | |||
179 | await server.views.simulateView({ id: videoId }) | ||
180 | await server.views.simulateView({ id: videoId }) | ||
181 | await server.views.simulateView({ id: videoId }) | ||
182 | |||
183 | await wait(1500) | ||
184 | |||
185 | await server.views.simulateView({ id: videoId }) | ||
186 | await server.views.simulateView({ id: videoId }) | ||
187 | |||
188 | await wait(1500) | ||
189 | |||
190 | await server.views.simulateView({ id: videoId }) | ||
191 | await server.views.simulateView({ id: videoId }) | ||
192 | |||
193 | await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } }) | ||
194 | |||
195 | const video = await server.videos.get({ id: videoId }) | ||
196 | expect(video.views).to.equal(3) | ||
197 | }) | ||
198 | |||
199 | it('Should remove the video', async function () { | ||
200 | const video = await server.videos.get({ id: videoId }) | ||
201 | await server.videos.remove({ id: videoId }) | ||
202 | |||
203 | await checkVideoFilesWereRemoved({ video, server }) | ||
204 | }) | ||
205 | |||
206 | it('Should not have videos', async function () { | ||
207 | const { total, data } = await server.videos.list() | ||
208 | |||
209 | expect(total).to.equal(0) | ||
210 | expect(data).to.be.an('array') | ||
211 | expect(data).to.have.lengthOf(0) | ||
212 | }) | ||
213 | |||
214 | it('Should upload 6 videos', async function () { | ||
215 | this.timeout(120000) | ||
216 | |||
217 | const videos = new Set([ | ||
218 | 'video_short.mp4', 'video_short.ogv', 'video_short.webm', | ||
219 | 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' | ||
220 | ]) | ||
221 | |||
222 | for (const video of videos) { | ||
223 | const attributes = { | ||
224 | name: video + ' name', | ||
225 | description: video + ' description', | ||
226 | category: 2, | ||
227 | licence: 1, | ||
228 | language: 'en', | ||
229 | nsfw: true, | ||
230 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
231 | fixture: video | ||
232 | } | ||
233 | |||
234 | await server.videos.upload({ attributes, mode }) | ||
235 | } | ||
236 | }) | ||
237 | |||
238 | it('Should have the correct durations', async function () { | ||
239 | const { total, data } = await server.videos.list() | ||
240 | |||
241 | expect(total).to.equal(6) | ||
242 | expect(data).to.be.an('array') | ||
243 | expect(data).to.have.lengthOf(6) | ||
244 | |||
245 | const videosByName: { [ name: string ]: Video } = {} | ||
246 | data.forEach(v => { videosByName[v.name] = v }) | ||
247 | |||
248 | expect(videosByName['video_short.mp4 name'].duration).to.equal(5) | ||
249 | expect(videosByName['video_short.ogv name'].duration).to.equal(5) | ||
250 | expect(videosByName['video_short.webm name'].duration).to.equal(5) | ||
251 | expect(videosByName['video_short1.webm name'].duration).to.equal(10) | ||
252 | expect(videosByName['video_short2.webm name'].duration).to.equal(5) | ||
253 | expect(videosByName['video_short3.webm name'].duration).to.equal(5) | ||
254 | }) | ||
255 | |||
256 | it('Should have the correct thumbnails', async function () { | ||
257 | const { data } = await server.videos.list() | ||
258 | |||
259 | // For the next test | ||
260 | videosListBase = data | ||
261 | |||
262 | for (const video of data) { | ||
263 | const videoName = video.name.replace(' name', '') | ||
264 | await testImageGeneratedByFFmpeg(server.url, videoName, video.thumbnailPath) | ||
265 | } | ||
266 | }) | ||
267 | |||
268 | it('Should list only the two first videos', async function () { | ||
269 | const { total, data } = await server.videos.list({ start: 0, count: 2, sort: 'name' }) | ||
270 | |||
271 | expect(total).to.equal(6) | ||
272 | expect(data.length).to.equal(2) | ||
273 | expect(data[0].name).to.equal(videosListBase[0].name) | ||
274 | expect(data[1].name).to.equal(videosListBase[1].name) | ||
275 | }) | ||
276 | |||
277 | it('Should list only the next three videos', async function () { | ||
278 | const { total, data } = await server.videos.list({ start: 2, count: 3, sort: 'name' }) | ||
279 | |||
280 | expect(total).to.equal(6) | ||
281 | expect(data.length).to.equal(3) | ||
282 | expect(data[0].name).to.equal(videosListBase[2].name) | ||
283 | expect(data[1].name).to.equal(videosListBase[3].name) | ||
284 | expect(data[2].name).to.equal(videosListBase[4].name) | ||
285 | }) | ||
286 | |||
287 | it('Should list the last video', async function () { | ||
288 | const { total, data } = await server.videos.list({ start: 5, count: 6, sort: 'name' }) | ||
289 | |||
290 | expect(total).to.equal(6) | ||
291 | expect(data.length).to.equal(1) | ||
292 | expect(data[0].name).to.equal(videosListBase[5].name) | ||
293 | }) | ||
294 | |||
295 | it('Should not have the total field', async function () { | ||
296 | const { total, data } = await server.videos.list({ start: 5, count: 6, sort: 'name', skipCount: true }) | ||
297 | |||
298 | expect(total).to.not.exist | ||
299 | expect(data.length).to.equal(1) | ||
300 | expect(data[0].name).to.equal(videosListBase[5].name) | ||
301 | }) | ||
302 | |||
303 | it('Should list and sort by name in descending order', async function () { | ||
304 | const { total, data } = await server.videos.list({ sort: '-name' }) | ||
305 | |||
306 | expect(total).to.equal(6) | ||
307 | expect(data.length).to.equal(6) | ||
308 | expect(data[0].name).to.equal('video_short.webm name') | ||
309 | expect(data[1].name).to.equal('video_short.ogv name') | ||
310 | expect(data[2].name).to.equal('video_short.mp4 name') | ||
311 | expect(data[3].name).to.equal('video_short3.webm name') | ||
312 | expect(data[4].name).to.equal('video_short2.webm name') | ||
313 | expect(data[5].name).to.equal('video_short1.webm name') | ||
314 | |||
315 | videoId = data[3].uuid | ||
316 | videoId2 = data[5].uuid | ||
317 | }) | ||
318 | |||
319 | it('Should list and sort by trending in descending order', async function () { | ||
320 | const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-trending' }) | ||
321 | |||
322 | expect(total).to.equal(6) | ||
323 | expect(data.length).to.equal(2) | ||
324 | }) | ||
325 | |||
326 | it('Should list and sort by hotness in descending order', async function () { | ||
327 | const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-hot' }) | ||
328 | |||
329 | expect(total).to.equal(6) | ||
330 | expect(data.length).to.equal(2) | ||
331 | }) | ||
332 | |||
333 | it('Should list and sort by best in descending order', async function () { | ||
334 | const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-best' }) | ||
335 | |||
336 | expect(total).to.equal(6) | ||
337 | expect(data.length).to.equal(2) | ||
338 | }) | ||
339 | |||
340 | it('Should update a video', async function () { | ||
341 | const attributes = { | ||
342 | name: 'my super video updated', | ||
343 | category: 4, | ||
344 | licence: 2, | ||
345 | language: 'ar', | ||
346 | nsfw: false, | ||
347 | description: 'my super description updated', | ||
348 | commentsEnabled: false, | ||
349 | downloadEnabled: false, | ||
350 | tags: [ 'tagup1', 'tagup2' ] | ||
351 | } | ||
352 | await server.videos.update({ id: videoId, attributes }) | ||
353 | }) | ||
354 | |||
355 | it('Should have the video updated', async function () { | ||
356 | this.timeout(60000) | ||
357 | |||
358 | await waitJobs([ server ]) | ||
359 | |||
360 | const video = await server.videos.get({ id: videoId }) | ||
361 | |||
362 | await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: updateCheckAttributes() }) | ||
363 | }) | ||
364 | |||
365 | it('Should update only the tags of a video', async function () { | ||
366 | const attributes = { | ||
367 | tags: [ 'supertag', 'tag1', 'tag2' ] | ||
368 | } | ||
369 | await server.videos.update({ id: videoId, attributes }) | ||
370 | |||
371 | const video = await server.videos.get({ id: videoId }) | ||
372 | |||
373 | await completeVideoCheck({ | ||
374 | server, | ||
375 | originServer: server, | ||
376 | videoUUID: video.uuid, | ||
377 | attributes: Object.assign(updateCheckAttributes(), attributes) | ||
378 | }) | ||
379 | }) | ||
380 | |||
381 | it('Should update only the description of a video', async function () { | ||
382 | const attributes = { | ||
383 | description: 'hello everybody' | ||
384 | } | ||
385 | await server.videos.update({ id: videoId, attributes }) | ||
386 | |||
387 | const video = await server.videos.get({ id: videoId }) | ||
388 | |||
389 | await completeVideoCheck({ | ||
390 | server, | ||
391 | originServer: server, | ||
392 | videoUUID: video.uuid, | ||
393 | attributes: Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes) | ||
394 | }) | ||
395 | }) | ||
396 | |||
397 | it('Should like a video', async function () { | ||
398 | await server.videos.rate({ id: videoId, rating: 'like' }) | ||
399 | |||
400 | const video = await server.videos.get({ id: videoId }) | ||
401 | |||
402 | expect(video.likes).to.equal(1) | ||
403 | expect(video.dislikes).to.equal(0) | ||
404 | }) | ||
405 | |||
406 | it('Should dislike the same video', async function () { | ||
407 | await server.videos.rate({ id: videoId, rating: 'dislike' }) | ||
408 | |||
409 | const video = await server.videos.get({ id: videoId }) | ||
410 | |||
411 | expect(video.likes).to.equal(0) | ||
412 | expect(video.dislikes).to.equal(1) | ||
413 | }) | ||
414 | |||
415 | it('Should sort by originallyPublishedAt', async function () { | ||
416 | { | ||
417 | const now = new Date() | ||
418 | const attributes = { originallyPublishedAt: now.toISOString() } | ||
419 | await server.videos.update({ id: videoId, attributes }) | ||
420 | |||
421 | const { data } = await server.videos.list({ sort: '-originallyPublishedAt' }) | ||
422 | const names = data.map(v => v.name) | ||
423 | |||
424 | expect(names[0]).to.equal('my super video updated') | ||
425 | expect(names[1]).to.equal('video_short2.webm name') | ||
426 | expect(names[2]).to.equal('video_short1.webm name') | ||
427 | expect(names[3]).to.equal('video_short.webm name') | ||
428 | expect(names[4]).to.equal('video_short.ogv name') | ||
429 | expect(names[5]).to.equal('video_short.mp4 name') | ||
430 | } | ||
431 | |||
432 | { | ||
433 | const now = new Date() | ||
434 | const attributes = { originallyPublishedAt: now.toISOString() } | ||
435 | await server.videos.update({ id: videoId2, attributes }) | ||
436 | |||
437 | const { data } = await server.videos.list({ sort: '-originallyPublishedAt' }) | ||
438 | const names = data.map(v => v.name) | ||
439 | |||
440 | expect(names[0]).to.equal('video_short1.webm name') | ||
441 | expect(names[1]).to.equal('my super video updated') | ||
442 | expect(names[2]).to.equal('video_short2.webm name') | ||
443 | expect(names[3]).to.equal('video_short.webm name') | ||
444 | expect(names[4]).to.equal('video_short.ogv name') | ||
445 | expect(names[5]).to.equal('video_short.mp4 name') | ||
446 | } | ||
447 | }) | ||
448 | |||
449 | after(async function () { | ||
450 | await cleanupTests([ server ]) | ||
451 | }) | ||
452 | } | ||
453 | |||
454 | describe('Legacy upload', function () { | ||
455 | runSuite('legacy') | ||
456 | }) | ||
457 | |||
458 | describe('Resumable upload', function () { | ||
459 | runSuite('resumable') | ||
460 | }) | ||
461 | }) | ||
diff --git a/packages/tests/src/api/videos/video-captions.ts b/packages/tests/src/api/videos/video-captions.ts new file mode 100644 index 000000000..027022549 --- /dev/null +++ b/packages/tests/src/api/videos/video-captions.ts | |||
@@ -0,0 +1,189 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { testCaptionFile } from '@tests/shared/captions.js' | ||
14 | import { checkVideoFilesWereRemoved } from '@tests/shared/videos.js' | ||
15 | |||
16 | describe('Test video captions', function () { | ||
17 | const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | ||
18 | |||
19 | let servers: PeerTubeServer[] | ||
20 | let videoUUID: string | ||
21 | |||
22 | before(async function () { | ||
23 | this.timeout(60000) | ||
24 | |||
25 | servers = await createMultipleServers(2) | ||
26 | |||
27 | await setAccessTokensToServers(servers) | ||
28 | await doubleFollow(servers[0], servers[1]) | ||
29 | |||
30 | await waitJobs(servers) | ||
31 | |||
32 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video name' } }) | ||
33 | videoUUID = uuid | ||
34 | |||
35 | await waitJobs(servers) | ||
36 | }) | ||
37 | |||
38 | it('Should list the captions and return an empty list', async function () { | ||
39 | for (const server of servers) { | ||
40 | const body = await server.captions.list({ videoId: videoUUID }) | ||
41 | expect(body.total).to.equal(0) | ||
42 | expect(body.data).to.have.lengthOf(0) | ||
43 | } | ||
44 | }) | ||
45 | |||
46 | it('Should create two new captions', async function () { | ||
47 | this.timeout(30000) | ||
48 | |||
49 | await servers[0].captions.add({ | ||
50 | language: 'ar', | ||
51 | videoId: videoUUID, | ||
52 | fixture: 'subtitle-good1.vtt' | ||
53 | }) | ||
54 | |||
55 | await servers[0].captions.add({ | ||
56 | language: 'zh', | ||
57 | videoId: videoUUID, | ||
58 | fixture: 'subtitle-good2.vtt', | ||
59 | mimeType: 'application/octet-stream' | ||
60 | }) | ||
61 | |||
62 | await waitJobs(servers) | ||
63 | }) | ||
64 | |||
65 | it('Should list these uploaded captions', async function () { | ||
66 | for (const server of servers) { | ||
67 | const body = await server.captions.list({ videoId: videoUUID }) | ||
68 | expect(body.total).to.equal(2) | ||
69 | expect(body.data).to.have.lengthOf(2) | ||
70 | |||
71 | const caption1 = body.data[0] | ||
72 | expect(caption1.language.id).to.equal('ar') | ||
73 | expect(caption1.language.label).to.equal('Arabic') | ||
74 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$')) | ||
75 | await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 1.') | ||
76 | |||
77 | const caption2 = body.data[1] | ||
78 | expect(caption2.language.id).to.equal('zh') | ||
79 | expect(caption2.language.label).to.equal('Chinese') | ||
80 | expect(caption2.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$')) | ||
81 | await testCaptionFile(server.url, caption2.captionPath, 'Subtitle good 2.') | ||
82 | } | ||
83 | }) | ||
84 | |||
85 | it('Should replace an existing caption', async function () { | ||
86 | this.timeout(30000) | ||
87 | |||
88 | await servers[0].captions.add({ | ||
89 | language: 'ar', | ||
90 | videoId: videoUUID, | ||
91 | fixture: 'subtitle-good2.vtt' | ||
92 | }) | ||
93 | |||
94 | await waitJobs(servers) | ||
95 | }) | ||
96 | |||
97 | it('Should have this caption updated', async function () { | ||
98 | for (const server of servers) { | ||
99 | const body = await server.captions.list({ videoId: videoUUID }) | ||
100 | expect(body.total).to.equal(2) | ||
101 | expect(body.data).to.have.lengthOf(2) | ||
102 | |||
103 | const caption1 = body.data[0] | ||
104 | expect(caption1.language.id).to.equal('ar') | ||
105 | expect(caption1.language.label).to.equal('Arabic') | ||
106 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$')) | ||
107 | await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 2.') | ||
108 | } | ||
109 | }) | ||
110 | |||
111 | it('Should replace an existing caption with a srt file and convert it', async function () { | ||
112 | this.timeout(30000) | ||
113 | |||
114 | await servers[0].captions.add({ | ||
115 | language: 'ar', | ||
116 | videoId: videoUUID, | ||
117 | fixture: 'subtitle-good.srt' | ||
118 | }) | ||
119 | |||
120 | await waitJobs(servers) | ||
121 | |||
122 | // Cache invalidation | ||
123 | await wait(3000) | ||
124 | }) | ||
125 | |||
126 | it('Should have this caption updated and converted', async function () { | ||
127 | for (const server of servers) { | ||
128 | const body = await server.captions.list({ videoId: videoUUID }) | ||
129 | expect(body.total).to.equal(2) | ||
130 | expect(body.data).to.have.lengthOf(2) | ||
131 | |||
132 | const caption1 = body.data[0] | ||
133 | expect(caption1.language.id).to.equal('ar') | ||
134 | expect(caption1.language.label).to.equal('Arabic') | ||
135 | expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$')) | ||
136 | |||
137 | const expected = 'WEBVTT FILE\r\n' + | ||
138 | '\r\n' + | ||
139 | '1\r\n' + | ||
140 | '00:00:01.600 --> 00:00:04.200\r\n' + | ||
141 | 'English (US)\r\n' + | ||
142 | '\r\n' + | ||
143 | '2\r\n' + | ||
144 | '00:00:05.900 --> 00:00:07.999\r\n' + | ||
145 | 'This is a subtitle in American English\r\n' + | ||
146 | '\r\n' + | ||
147 | '3\r\n' + | ||
148 | '00:00:10.000 --> 00:00:14.000\r\n' + | ||
149 | 'Adding subtitles is very easy to do\r\n' | ||
150 | await testCaptionFile(server.url, caption1.captionPath, expected) | ||
151 | } | ||
152 | }) | ||
153 | |||
154 | it('Should remove one caption', async function () { | ||
155 | this.timeout(30000) | ||
156 | |||
157 | await servers[0].captions.delete({ videoId: videoUUID, language: 'ar' }) | ||
158 | |||
159 | await waitJobs(servers) | ||
160 | }) | ||
161 | |||
162 | it('Should only list the caption that was not deleted', async function () { | ||
163 | for (const server of servers) { | ||
164 | const body = await server.captions.list({ videoId: videoUUID }) | ||
165 | expect(body.total).to.equal(1) | ||
166 | expect(body.data).to.have.lengthOf(1) | ||
167 | |||
168 | const caption = body.data[0] | ||
169 | |||
170 | expect(caption.language.id).to.equal('zh') | ||
171 | expect(caption.language.label).to.equal('Chinese') | ||
172 | expect(caption.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$')) | ||
173 | await testCaptionFile(server.url, caption.captionPath, 'Subtitle good 2.') | ||
174 | } | ||
175 | }) | ||
176 | |||
177 | it('Should remove the video, and thus all video captions', async function () { | ||
178 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
179 | const { data: captions } = await servers[0].captions.list({ videoId: videoUUID }) | ||
180 | |||
181 | await servers[0].videos.remove({ id: videoUUID }) | ||
182 | |||
183 | await checkVideoFilesWereRemoved({ server: servers[0], video, captions }) | ||
184 | }) | ||
185 | |||
186 | after(async function () { | ||
187 | await cleanupTests(servers) | ||
188 | }) | ||
189 | }) | ||
diff --git a/packages/tests/src/api/videos/video-change-ownership.ts b/packages/tests/src/api/videos/video-change-ownership.ts new file mode 100644 index 000000000..717c37469 --- /dev/null +++ b/packages/tests/src/api/videos/video-change-ownership.ts | |||
@@ -0,0 +1,314 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | ChangeOwnershipCommand, | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | createSingleServer, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
16 | |||
17 | describe('Test video change ownership - nominal', function () { | ||
18 | let servers: PeerTubeServer[] = [] | ||
19 | |||
20 | const firstUser = 'first' | ||
21 | const secondUser = 'second' | ||
22 | |||
23 | let firstUserToken = '' | ||
24 | let firstUserChannelId: number | ||
25 | |||
26 | let secondUserToken = '' | ||
27 | let secondUserChannelId: number | ||
28 | |||
29 | let lastRequestId: number | ||
30 | |||
31 | let liveId: number | ||
32 | |||
33 | let command: ChangeOwnershipCommand | ||
34 | |||
35 | before(async function () { | ||
36 | this.timeout(240000) | ||
37 | |||
38 | servers = await createMultipleServers(2) | ||
39 | await setAccessTokensToServers(servers) | ||
40 | await setDefaultVideoChannel(servers) | ||
41 | |||
42 | await servers[0].config.updateCustomSubConfig({ | ||
43 | newConfig: { | ||
44 | transcoding: { | ||
45 | enabled: false | ||
46 | }, | ||
47 | live: { | ||
48 | enabled: true | ||
49 | } | ||
50 | } | ||
51 | }) | ||
52 | |||
53 | firstUserToken = await servers[0].users.generateUserAndToken(firstUser) | ||
54 | secondUserToken = await servers[0].users.generateUserAndToken(secondUser) | ||
55 | |||
56 | { | ||
57 | const { videoChannels } = await servers[0].users.getMyInfo({ token: firstUserToken }) | ||
58 | firstUserChannelId = videoChannels[0].id | ||
59 | } | ||
60 | |||
61 | { | ||
62 | const { videoChannels } = await servers[0].users.getMyInfo({ token: secondUserToken }) | ||
63 | secondUserChannelId = videoChannels[0].id | ||
64 | } | ||
65 | |||
66 | { | ||
67 | const attributes = { | ||
68 | name: 'my super name', | ||
69 | description: 'my super description' | ||
70 | } | ||
71 | const { id } = await servers[0].videos.upload({ token: firstUserToken, attributes }) | ||
72 | |||
73 | servers[0].store.videoCreated = await servers[0].videos.get({ id }) | ||
74 | } | ||
75 | |||
76 | { | ||
77 | const attributes = { name: 'live', channelId: firstUserChannelId, privacy: VideoPrivacy.PUBLIC } | ||
78 | const video = await servers[0].live.create({ token: firstUserToken, fields: attributes }) | ||
79 | |||
80 | liveId = video.id | ||
81 | } | ||
82 | |||
83 | command = servers[0].changeOwnership | ||
84 | |||
85 | await doubleFollow(servers[0], servers[1]) | ||
86 | }) | ||
87 | |||
88 | it('Should not have video change ownership', async function () { | ||
89 | { | ||
90 | const body = await command.list({ token: firstUserToken }) | ||
91 | |||
92 | expect(body.total).to.equal(0) | ||
93 | expect(body.data).to.be.an('array') | ||
94 | expect(body.data.length).to.equal(0) | ||
95 | } | ||
96 | |||
97 | { | ||
98 | const body = await command.list({ token: secondUserToken }) | ||
99 | |||
100 | expect(body.total).to.equal(0) | ||
101 | expect(body.data).to.be.an('array') | ||
102 | expect(body.data.length).to.equal(0) | ||
103 | } | ||
104 | }) | ||
105 | |||
106 | it('Should send a request to change ownership of a video', async function () { | ||
107 | this.timeout(15000) | ||
108 | |||
109 | await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser }) | ||
110 | }) | ||
111 | |||
112 | it('Should only return a request to change ownership for the second user', async function () { | ||
113 | { | ||
114 | const body = await command.list({ token: firstUserToken }) | ||
115 | |||
116 | expect(body.total).to.equal(0) | ||
117 | expect(body.data).to.be.an('array') | ||
118 | expect(body.data.length).to.equal(0) | ||
119 | } | ||
120 | |||
121 | { | ||
122 | const body = await command.list({ token: secondUserToken }) | ||
123 | |||
124 | expect(body.total).to.equal(1) | ||
125 | expect(body.data).to.be.an('array') | ||
126 | expect(body.data.length).to.equal(1) | ||
127 | |||
128 | lastRequestId = body.data[0].id | ||
129 | } | ||
130 | }) | ||
131 | |||
132 | it('Should accept the same change ownership request without crashing', async function () { | ||
133 | await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser }) | ||
134 | }) | ||
135 | |||
136 | it('Should not create multiple change ownership requests while one is waiting', async function () { | ||
137 | const body = await command.list({ token: secondUserToken }) | ||
138 | |||
139 | expect(body.total).to.equal(1) | ||
140 | expect(body.data).to.be.an('array') | ||
141 | expect(body.data.length).to.equal(1) | ||
142 | }) | ||
143 | |||
144 | it('Should not be possible to refuse the change of ownership from first user', async function () { | ||
145 | await command.refuse({ token: firstUserToken, ownershipId: lastRequestId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
146 | }) | ||
147 | |||
148 | it('Should be possible to refuse the change of ownership from second user', async function () { | ||
149 | await command.refuse({ token: secondUserToken, ownershipId: lastRequestId }) | ||
150 | }) | ||
151 | |||
152 | it('Should send a new request to change ownership of a video', async function () { | ||
153 | this.timeout(15000) | ||
154 | |||
155 | await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser }) | ||
156 | }) | ||
157 | |||
158 | it('Should return two requests to change ownership for the second user', async function () { | ||
159 | { | ||
160 | const body = await command.list({ token: firstUserToken }) | ||
161 | |||
162 | expect(body.total).to.equal(0) | ||
163 | expect(body.data).to.be.an('array') | ||
164 | expect(body.data.length).to.equal(0) | ||
165 | } | ||
166 | |||
167 | { | ||
168 | const body = await command.list({ token: secondUserToken }) | ||
169 | |||
170 | expect(body.total).to.equal(2) | ||
171 | expect(body.data).to.be.an('array') | ||
172 | expect(body.data.length).to.equal(2) | ||
173 | |||
174 | lastRequestId = body.data[0].id | ||
175 | } | ||
176 | }) | ||
177 | |||
178 | it('Should not be possible to accept the change of ownership from first user', async function () { | ||
179 | await command.accept({ | ||
180 | token: firstUserToken, | ||
181 | ownershipId: lastRequestId, | ||
182 | channelId: secondUserChannelId, | ||
183 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
184 | }) | ||
185 | }) | ||
186 | |||
187 | it('Should be possible to accept the change of ownership from second user', async function () { | ||
188 | await command.accept({ token: secondUserToken, ownershipId: lastRequestId, channelId: secondUserChannelId }) | ||
189 | |||
190 | await waitJobs(servers) | ||
191 | }) | ||
192 | |||
193 | it('Should have the channel of the video updated', async function () { | ||
194 | for (const server of servers) { | ||
195 | const video = await server.videos.get({ id: servers[0].store.videoCreated.uuid }) | ||
196 | |||
197 | expect(video.name).to.equal('my super name') | ||
198 | expect(video.channel.displayName).to.equal('Main second channel') | ||
199 | expect(video.channel.name).to.equal('second_channel') | ||
200 | } | ||
201 | }) | ||
202 | |||
203 | it('Should send a request to change ownership of a live', async function () { | ||
204 | this.timeout(15000) | ||
205 | |||
206 | await command.create({ token: firstUserToken, videoId: liveId, username: secondUser }) | ||
207 | |||
208 | const body = await command.list({ token: secondUserToken }) | ||
209 | |||
210 | expect(body.total).to.equal(3) | ||
211 | expect(body.data.length).to.equal(3) | ||
212 | |||
213 | lastRequestId = body.data[0].id | ||
214 | }) | ||
215 | |||
216 | it('Should accept a live ownership change', async function () { | ||
217 | this.timeout(20000) | ||
218 | |||
219 | await command.accept({ token: secondUserToken, ownershipId: lastRequestId, channelId: secondUserChannelId }) | ||
220 | |||
221 | await waitJobs(servers) | ||
222 | |||
223 | for (const server of servers) { | ||
224 | const video = await server.videos.get({ id: servers[0].store.videoCreated.uuid }) | ||
225 | |||
226 | expect(video.name).to.equal('my super name') | ||
227 | expect(video.channel.displayName).to.equal('Main second channel') | ||
228 | expect(video.channel.name).to.equal('second_channel') | ||
229 | } | ||
230 | }) | ||
231 | |||
232 | after(async function () { | ||
233 | await cleanupTests(servers) | ||
234 | }) | ||
235 | }) | ||
236 | |||
237 | describe('Test video change ownership - quota too small', function () { | ||
238 | let server: PeerTubeServer | ||
239 | const firstUser = 'first' | ||
240 | const secondUser = 'second' | ||
241 | |||
242 | let firstUserToken = '' | ||
243 | let secondUserToken = '' | ||
244 | let lastRequestId: number | ||
245 | |||
246 | before(async function () { | ||
247 | this.timeout(50000) | ||
248 | |||
249 | // Run one server | ||
250 | server = await createSingleServer(1) | ||
251 | await setAccessTokensToServers([ server ]) | ||
252 | |||
253 | await server.users.create({ username: secondUser, videoQuota: 10 }) | ||
254 | |||
255 | firstUserToken = await server.users.generateUserAndToken(firstUser) | ||
256 | secondUserToken = await server.login.getAccessToken(secondUser) | ||
257 | |||
258 | // Upload some videos on the server | ||
259 | const attributes = { | ||
260 | name: 'my super name', | ||
261 | description: 'my super description' | ||
262 | } | ||
263 | await server.videos.upload({ token: firstUserToken, attributes }) | ||
264 | |||
265 | await waitJobs(server) | ||
266 | |||
267 | const { data } = await server.videos.list() | ||
268 | expect(data.length).to.equal(1) | ||
269 | |||
270 | server.store.videoCreated = data.find(video => video.name === 'my super name') | ||
271 | }) | ||
272 | |||
273 | it('Should send a request to change ownership of a video', async function () { | ||
274 | this.timeout(15000) | ||
275 | |||
276 | await server.changeOwnership.create({ token: firstUserToken, videoId: server.store.videoCreated.id, username: secondUser }) | ||
277 | }) | ||
278 | |||
279 | it('Should only return a request to change ownership for the second user', async function () { | ||
280 | { | ||
281 | const body = await server.changeOwnership.list({ token: firstUserToken }) | ||
282 | |||
283 | expect(body.total).to.equal(0) | ||
284 | expect(body.data).to.be.an('array') | ||
285 | expect(body.data.length).to.equal(0) | ||
286 | } | ||
287 | |||
288 | { | ||
289 | const body = await server.changeOwnership.list({ token: secondUserToken }) | ||
290 | |||
291 | expect(body.total).to.equal(1) | ||
292 | expect(body.data).to.be.an('array') | ||
293 | expect(body.data.length).to.equal(1) | ||
294 | |||
295 | lastRequestId = body.data[0].id | ||
296 | } | ||
297 | }) | ||
298 | |||
299 | it('Should not be possible to accept the change of ownership from second user because of exceeded quota', async function () { | ||
300 | const { videoChannels } = await server.users.getMyInfo({ token: secondUserToken }) | ||
301 | const channelId = videoChannels[0].id | ||
302 | |||
303 | await server.changeOwnership.accept({ | ||
304 | token: secondUserToken, | ||
305 | ownershipId: lastRequestId, | ||
306 | channelId, | ||
307 | expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413 | ||
308 | }) | ||
309 | }) | ||
310 | |||
311 | after(async function () { | ||
312 | await cleanupTests([ server ]) | ||
313 | }) | ||
314 | }) | ||
diff --git a/packages/tests/src/api/videos/video-channel-syncs.ts b/packages/tests/src/api/videos/video-channel-syncs.ts new file mode 100644 index 000000000..54212bcb5 --- /dev/null +++ b/packages/tests/src/api/videos/video-channel-syncs.ts | |||
@@ -0,0 +1,321 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { VideoChannelSyncState, VideoInclude, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | getServerImportConfig, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar, | ||
14 | setDefaultVideoChannel, | ||
15 | waitJobs | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
18 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
19 | |||
20 | describe('Test channel synchronizations', function () { | ||
21 | if (areHttpImportTestsDisabled()) return | ||
22 | |||
23 | function runSuite (mode: 'youtube-dl' | 'yt-dlp') { | ||
24 | |||
25 | describe('Sync using ' + mode, function () { | ||
26 | let servers: PeerTubeServer[] | ||
27 | let sqlCommands: SQLCommand[] = [] | ||
28 | |||
29 | let startTestDate: Date | ||
30 | |||
31 | let rootChannelSyncId: number | ||
32 | const userInfo = { | ||
33 | accessToken: '', | ||
34 | username: 'user1', | ||
35 | channelName: 'user1_channel', | ||
36 | channelId: -1, | ||
37 | syncId: -1 | ||
38 | } | ||
39 | |||
40 | async function changeDateForSync (channelSyncId: number, newDate: string) { | ||
41 | await sqlCommands[0].updateQuery( | ||
42 | `UPDATE "videoChannelSync" ` + | ||
43 | `SET "createdAt"='${newDate}', "lastSyncAt"='${newDate}' ` + | ||
44 | `WHERE id=${channelSyncId}` | ||
45 | ) | ||
46 | } | ||
47 | |||
48 | async function listAllVideosOfChannel (channelName: string) { | ||
49 | return servers[0].videos.listByChannel({ | ||
50 | handle: channelName, | ||
51 | include: VideoInclude.NOT_PUBLISHED_STATE | ||
52 | }) | ||
53 | } | ||
54 | |||
55 | async function forceSyncAll (videoChannelSyncId: number, fromDate = '1970-01-01') { | ||
56 | await changeDateForSync(videoChannelSyncId, fromDate) | ||
57 | |||
58 | await servers[0].debug.sendCommand({ | ||
59 | body: { | ||
60 | command: 'process-video-channel-sync-latest' | ||
61 | } | ||
62 | }) | ||
63 | |||
64 | await waitJobs(servers) | ||
65 | } | ||
66 | |||
67 | before(async function () { | ||
68 | this.timeout(240_000) | ||
69 | |||
70 | startTestDate = new Date() | ||
71 | |||
72 | servers = await createMultipleServers(2, getServerImportConfig(mode)) | ||
73 | |||
74 | await setAccessTokensToServers(servers) | ||
75 | await setDefaultVideoChannel(servers) | ||
76 | await setDefaultChannelAvatar(servers) | ||
77 | await setDefaultAccountAvatar(servers) | ||
78 | |||
79 | await servers[0].config.enableChannelSync() | ||
80 | |||
81 | { | ||
82 | userInfo.accessToken = await servers[0].users.generateUserAndToken(userInfo.username) | ||
83 | |||
84 | const { videoChannels } = await servers[0].users.getMyInfo({ token: userInfo.accessToken }) | ||
85 | userInfo.channelId = videoChannels[0].id | ||
86 | } | ||
87 | |||
88 | sqlCommands = servers.map(s => new SQLCommand(s)) | ||
89 | }) | ||
90 | |||
91 | it('Should fetch the latest channel videos of a remote channel', async function () { | ||
92 | this.timeout(120_000) | ||
93 | |||
94 | { | ||
95 | const { video } = await servers[0].imports.importVideo({ | ||
96 | attributes: { | ||
97 | channelId: servers[0].store.channel.id, | ||
98 | privacy: VideoPrivacy.PUBLIC, | ||
99 | targetUrl: FIXTURE_URLS.youtube | ||
100 | } | ||
101 | }) | ||
102 | |||
103 | expect(video.name).to.equal('small video - youtube') | ||
104 | expect(video.waitTranscoding).to.be.true | ||
105 | |||
106 | const { total } = await listAllVideosOfChannel('root_channel') | ||
107 | expect(total).to.equal(1) | ||
108 | } | ||
109 | |||
110 | const { videoChannelSync } = await servers[0].channelSyncs.create({ | ||
111 | attributes: { | ||
112 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
113 | videoChannelId: servers[0].store.channel.id | ||
114 | } | ||
115 | }) | ||
116 | rootChannelSyncId = videoChannelSync.id | ||
117 | |||
118 | await forceSyncAll(rootChannelSyncId) | ||
119 | |||
120 | { | ||
121 | const { total, data } = await listAllVideosOfChannel('root_channel') | ||
122 | expect(total).to.equal(2) | ||
123 | expect(data[0].name).to.equal('test') | ||
124 | expect(data[0].waitTranscoding).to.be.true | ||
125 | } | ||
126 | }) | ||
127 | |||
128 | it('Should add another synchronization', async function () { | ||
129 | const externalChannelUrl = FIXTURE_URLS.youtubeChannel + '?foo=bar' | ||
130 | |||
131 | const { videoChannelSync } = await servers[0].channelSyncs.create({ | ||
132 | attributes: { | ||
133 | externalChannelUrl, | ||
134 | videoChannelId: servers[0].store.channel.id | ||
135 | } | ||
136 | }) | ||
137 | |||
138 | expect(videoChannelSync.externalChannelUrl).to.equal(externalChannelUrl) | ||
139 | expect(videoChannelSync.channel.id).to.equal(servers[0].store.channel.id) | ||
140 | expect(videoChannelSync.channel.name).to.equal('root_channel') | ||
141 | expect(videoChannelSync.state.id).to.equal(VideoChannelSyncState.WAITING_FIRST_RUN) | ||
142 | expect(new Date(videoChannelSync.createdAt)).to.be.above(startTestDate).and.to.be.at.most(new Date()) | ||
143 | }) | ||
144 | |||
145 | it('Should add a synchronization for another user', async function () { | ||
146 | const { videoChannelSync } = await servers[0].channelSyncs.create({ | ||
147 | attributes: { | ||
148 | externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?baz=qux', | ||
149 | videoChannelId: userInfo.channelId | ||
150 | }, | ||
151 | token: userInfo.accessToken | ||
152 | }) | ||
153 | userInfo.syncId = videoChannelSync.id | ||
154 | }) | ||
155 | |||
156 | it('Should not import a channel if not asked', async function () { | ||
157 | await waitJobs(servers) | ||
158 | |||
159 | const { data } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username }) | ||
160 | |||
161 | expect(data[0].state).to.contain({ | ||
162 | id: VideoChannelSyncState.WAITING_FIRST_RUN, | ||
163 | label: 'Waiting first run' | ||
164 | }) | ||
165 | }) | ||
166 | |||
167 | it('Should only fetch the videos newer than the creation date', async function () { | ||
168 | this.timeout(120_000) | ||
169 | |||
170 | await forceSyncAll(userInfo.syncId, '2019-03-01') | ||
171 | |||
172 | const { data, total } = await listAllVideosOfChannel(userInfo.channelName) | ||
173 | |||
174 | expect(total).to.equal(1) | ||
175 | expect(data[0].name).to.equal('test') | ||
176 | }) | ||
177 | |||
178 | it('Should list channel synchronizations', async function () { | ||
179 | // Root | ||
180 | { | ||
181 | const { total, data } = await servers[0].channelSyncs.listByAccount({ accountName: 'root' }) | ||
182 | expect(total).to.equal(2) | ||
183 | |||
184 | expect(data[0]).to.deep.contain({ | ||
185 | externalChannelUrl: FIXTURE_URLS.youtubeChannel, | ||
186 | state: { | ||
187 | id: VideoChannelSyncState.SYNCED, | ||
188 | label: 'Synchronized' | ||
189 | } | ||
190 | }) | ||
191 | |||
192 | expect(new Date(data[0].lastSyncAt)).to.be.greaterThan(startTestDate) | ||
193 | |||
194 | expect(data[0].channel).to.contain({ id: servers[0].store.channel.id }) | ||
195 | expect(data[1]).to.contain({ externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?foo=bar' }) | ||
196 | } | ||
197 | |||
198 | // User | ||
199 | { | ||
200 | const { total, data } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username }) | ||
201 | expect(total).to.equal(1) | ||
202 | expect(data[0]).to.deep.contain({ | ||
203 | externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?baz=qux', | ||
204 | state: { | ||
205 | id: VideoChannelSyncState.SYNCED, | ||
206 | label: 'Synchronized' | ||
207 | } | ||
208 | }) | ||
209 | } | ||
210 | }) | ||
211 | |||
212 | it('Should list imports of a channel synchronization', async function () { | ||
213 | const { total, data } = await servers[0].imports.getMyVideoImports({ videoChannelSyncId: rootChannelSyncId }) | ||
214 | |||
215 | expect(total).to.equal(1) | ||
216 | expect(data).to.have.lengthOf(1) | ||
217 | expect(data[0].video.name).to.equal('test') | ||
218 | }) | ||
219 | |||
220 | it('Should remove user\'s channel synchronizations', async function () { | ||
221 | await servers[0].channelSyncs.delete({ channelSyncId: userInfo.syncId }) | ||
222 | |||
223 | const { total } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username }) | ||
224 | expect(total).to.equal(0) | ||
225 | }) | ||
226 | |||
227 | // FIXME: youtube-dl/yt-dlp doesn't work when speicifying a port after the hostname | ||
228 | // it('Should import a remote PeerTube channel', async function () { | ||
229 | // this.timeout(240_000) | ||
230 | |||
231 | // await servers[1].videos.quickUpload({ name: 'remote 1' }) | ||
232 | // await waitJobs(servers) | ||
233 | |||
234 | // const { videoChannelSync } = await servers[0].channelSyncs.create({ | ||
235 | // attributes: { | ||
236 | // externalChannelUrl: servers[1].url + '/c/root_channel', | ||
237 | // videoChannelId: userInfo.channelId | ||
238 | // }, | ||
239 | // token: userInfo.accessToken | ||
240 | // }) | ||
241 | // await servers[0].channels.importVideos({ | ||
242 | // channelName: userInfo.channelName, | ||
243 | // externalChannelUrl: servers[1].url + '/c/root_channel', | ||
244 | // videoChannelSyncId: videoChannelSync.id, | ||
245 | // token: userInfo.accessToken | ||
246 | // }) | ||
247 | |||
248 | // await waitJobs(servers) | ||
249 | |||
250 | // const { data, total } = await servers[0].videos.listByChannel({ | ||
251 | // handle: userInfo.channelName, | ||
252 | // include: VideoInclude.NOT_PUBLISHED_STATE | ||
253 | // }) | ||
254 | |||
255 | // expect(total).to.equal(2) | ||
256 | // expect(data[0].name).to.equal('remote 1') | ||
257 | // }) | ||
258 | |||
259 | // it('Should keep synced a remote PeerTube channel', async function () { | ||
260 | // this.timeout(240_000) | ||
261 | |||
262 | // await servers[1].videos.quickUpload({ name: 'remote 2' }) | ||
263 | // await waitJobs(servers) | ||
264 | |||
265 | // await servers[0].debug.sendCommand({ | ||
266 | // body: { | ||
267 | // command: 'process-video-channel-sync-latest' | ||
268 | // } | ||
269 | // }) | ||
270 | |||
271 | // await waitJobs(servers) | ||
272 | |||
273 | // const { data, total } = await servers[0].videos.listByChannel({ | ||
274 | // handle: userInfo.channelName, | ||
275 | // include: VideoInclude.NOT_PUBLISHED_STATE | ||
276 | // }) | ||
277 | // expect(total).to.equal(2) | ||
278 | // expect(data[0].name).to.equal('remote 2') | ||
279 | // }) | ||
280 | |||
281 | it('Should fetch the latest videos of a youtube playlist', async function () { | ||
282 | this.timeout(120_000) | ||
283 | |||
284 | const { id: channelId } = await servers[0].channels.create({ | ||
285 | attributes: { | ||
286 | name: 'channel2' | ||
287 | } | ||
288 | }) | ||
289 | |||
290 | const { videoChannelSync: { id: videoChannelSyncId } } = await servers[0].channelSyncs.create({ | ||
291 | attributes: { | ||
292 | externalChannelUrl: FIXTURE_URLS.youtubePlaylist, | ||
293 | videoChannelId: channelId | ||
294 | } | ||
295 | }) | ||
296 | |||
297 | await forceSyncAll(videoChannelSyncId) | ||
298 | |||
299 | { | ||
300 | |||
301 | const { total, data } = await listAllVideosOfChannel('channel2') | ||
302 | expect(total).to.equal(2) | ||
303 | expect(data[0].name).to.equal('test') | ||
304 | expect(data[1].name).to.equal('small video - youtube') | ||
305 | } | ||
306 | }) | ||
307 | |||
308 | after(async function () { | ||
309 | for (const sqlCommand of sqlCommands) { | ||
310 | await sqlCommand.cleanup() | ||
311 | } | ||
312 | |||
313 | await cleanupTests(servers) | ||
314 | }) | ||
315 | }) | ||
316 | } | ||
317 | |||
318 | // FIXME: suite is broken with youtube-dl | ||
319 | // runSuite('youtube-dl') | ||
320 | runSuite('yt-dlp') | ||
321 | }) | ||
diff --git a/packages/tests/src/api/videos/video-channels.ts b/packages/tests/src/api/videos/video-channels.ts new file mode 100644 index 000000000..64b1b9315 --- /dev/null +++ b/packages/tests/src/api/videos/video-channels.ts | |||
@@ -0,0 +1,556 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename } from 'path' | ||
5 | import { ACTOR_IMAGES_SIZE } from '@peertube/peertube-server/server/initializers/constants.js' | ||
6 | import { testFileExistsOrNot, testImage } from '@tests/shared/checks.js' | ||
7 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
8 | import { wait } from '@peertube/peertube-core-utils' | ||
9 | import { ActorImageType, User, VideoChannel } from '@peertube/peertube-models' | ||
10 | import { | ||
11 | cleanupTests, | ||
12 | createMultipleServers, | ||
13 | doubleFollow, | ||
14 | PeerTubeServer, | ||
15 | setAccessTokensToServers, | ||
16 | setDefaultAccountAvatar, | ||
17 | setDefaultVideoChannel, | ||
18 | waitJobs | ||
19 | } from '@peertube/peertube-server-commands' | ||
20 | |||
21 | async function findChannel (server: PeerTubeServer, channelId: number) { | ||
22 | const body = await server.channels.list({ sort: '-name' }) | ||
23 | |||
24 | return body.data.find(c => c.id === channelId) | ||
25 | } | ||
26 | |||
27 | describe('Test video channels', function () { | ||
28 | let servers: PeerTubeServer[] | ||
29 | let sqlCommands: SQLCommand[] = [] | ||
30 | |||
31 | let userInfo: User | ||
32 | let secondVideoChannelId: number | ||
33 | let totoChannel: number | ||
34 | let videoUUID: string | ||
35 | let accountName: string | ||
36 | let secondUserChannelName: string | ||
37 | |||
38 | const avatarPaths: { [ port: number ]: string } = {} | ||
39 | const bannerPaths: { [ port: number ]: string } = {} | ||
40 | |||
41 | before(async function () { | ||
42 | this.timeout(60000) | ||
43 | |||
44 | servers = await createMultipleServers(2) | ||
45 | |||
46 | await setAccessTokensToServers(servers) | ||
47 | await setDefaultVideoChannel(servers) | ||
48 | await setDefaultAccountAvatar(servers) | ||
49 | |||
50 | await doubleFollow(servers[0], servers[1]) | ||
51 | |||
52 | sqlCommands = servers.map(s => new SQLCommand(s)) | ||
53 | }) | ||
54 | |||
55 | it('Should have one video channel (created with root)', async () => { | ||
56 | const body = await servers[0].channels.list({ start: 0, count: 2 }) | ||
57 | |||
58 | expect(body.total).to.equal(1) | ||
59 | expect(body.data).to.be.an('array') | ||
60 | expect(body.data).to.have.lengthOf(1) | ||
61 | }) | ||
62 | |||
63 | it('Should create another video channel', async function () { | ||
64 | this.timeout(30000) | ||
65 | |||
66 | { | ||
67 | const videoChannel = { | ||
68 | name: 'second_video_channel', | ||
69 | displayName: 'second video channel', | ||
70 | description: 'super video channel description', | ||
71 | support: 'super video channel support text' | ||
72 | } | ||
73 | const created = await servers[0].channels.create({ attributes: videoChannel }) | ||
74 | secondVideoChannelId = created.id | ||
75 | } | ||
76 | |||
77 | // The channel is 1 is propagated to servers 2 | ||
78 | { | ||
79 | const attributes = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' } | ||
80 | const { uuid } = await servers[0].videos.upload({ attributes }) | ||
81 | videoUUID = uuid | ||
82 | } | ||
83 | |||
84 | await waitJobs(servers) | ||
85 | }) | ||
86 | |||
87 | it('Should have two video channels when getting my information', async () => { | ||
88 | userInfo = await servers[0].users.getMyInfo() | ||
89 | |||
90 | expect(userInfo.videoChannels).to.be.an('array') | ||
91 | expect(userInfo.videoChannels).to.have.lengthOf(2) | ||
92 | |||
93 | const videoChannels = userInfo.videoChannels | ||
94 | expect(videoChannels[0].name).to.equal('root_channel') | ||
95 | expect(videoChannels[0].displayName).to.equal('Main root channel') | ||
96 | |||
97 | expect(videoChannels[1].name).to.equal('second_video_channel') | ||
98 | expect(videoChannels[1].displayName).to.equal('second video channel') | ||
99 | expect(videoChannels[1].description).to.equal('super video channel description') | ||
100 | expect(videoChannels[1].support).to.equal('super video channel support text') | ||
101 | |||
102 | accountName = userInfo.account.name + '@' + userInfo.account.host | ||
103 | }) | ||
104 | |||
105 | it('Should have two video channels when getting account channels on server 1', async function () { | ||
106 | const body = await servers[0].channels.listByAccount({ accountName }) | ||
107 | expect(body.total).to.equal(2) | ||
108 | |||
109 | const videoChannels = body.data | ||
110 | |||
111 | expect(videoChannels).to.be.an('array') | ||
112 | expect(videoChannels).to.have.lengthOf(2) | ||
113 | |||
114 | expect(videoChannels[0].name).to.equal('root_channel') | ||
115 | expect(videoChannels[0].displayName).to.equal('Main root channel') | ||
116 | |||
117 | expect(videoChannels[1].name).to.equal('second_video_channel') | ||
118 | expect(videoChannels[1].displayName).to.equal('second video channel') | ||
119 | expect(videoChannels[1].description).to.equal('super video channel description') | ||
120 | expect(videoChannels[1].support).to.equal('super video channel support text') | ||
121 | }) | ||
122 | |||
123 | it('Should paginate and sort account channels', async function () { | ||
124 | { | ||
125 | const body = await servers[0].channels.listByAccount({ | ||
126 | accountName, | ||
127 | start: 0, | ||
128 | count: 1, | ||
129 | sort: 'createdAt' | ||
130 | }) | ||
131 | |||
132 | expect(body.total).to.equal(2) | ||
133 | expect(body.data).to.have.lengthOf(1) | ||
134 | |||
135 | const videoChannel: VideoChannel = body.data[0] | ||
136 | expect(videoChannel.name).to.equal('root_channel') | ||
137 | } | ||
138 | |||
139 | { | ||
140 | const body = await servers[0].channels.listByAccount({ | ||
141 | accountName, | ||
142 | start: 0, | ||
143 | count: 1, | ||
144 | sort: '-createdAt' | ||
145 | }) | ||
146 | |||
147 | expect(body.total).to.equal(2) | ||
148 | expect(body.data).to.have.lengthOf(1) | ||
149 | expect(body.data[0].name).to.equal('second_video_channel') | ||
150 | } | ||
151 | |||
152 | { | ||
153 | const body = await servers[0].channels.listByAccount({ | ||
154 | accountName, | ||
155 | start: 1, | ||
156 | count: 1, | ||
157 | sort: '-createdAt' | ||
158 | }) | ||
159 | |||
160 | expect(body.total).to.equal(2) | ||
161 | expect(body.data).to.have.lengthOf(1) | ||
162 | expect(body.data[0].name).to.equal('root_channel') | ||
163 | } | ||
164 | }) | ||
165 | |||
166 | it('Should have one video channel when getting account channels on server 2', async function () { | ||
167 | const body = await servers[1].channels.listByAccount({ accountName }) | ||
168 | |||
169 | expect(body.total).to.equal(1) | ||
170 | expect(body.data).to.be.an('array') | ||
171 | expect(body.data).to.have.lengthOf(1) | ||
172 | |||
173 | const videoChannel = body.data[0] | ||
174 | expect(videoChannel.name).to.equal('second_video_channel') | ||
175 | expect(videoChannel.displayName).to.equal('second video channel') | ||
176 | expect(videoChannel.description).to.equal('super video channel description') | ||
177 | expect(videoChannel.support).to.equal('super video channel support text') | ||
178 | }) | ||
179 | |||
180 | it('Should list video channels', async function () { | ||
181 | const body = await servers[0].channels.list({ start: 1, count: 1, sort: '-name' }) | ||
182 | |||
183 | expect(body.total).to.equal(2) | ||
184 | expect(body.data).to.be.an('array') | ||
185 | expect(body.data).to.have.lengthOf(1) | ||
186 | expect(body.data[0].name).to.equal('root_channel') | ||
187 | expect(body.data[0].displayName).to.equal('Main root channel') | ||
188 | }) | ||
189 | |||
190 | it('Should update video channel', async function () { | ||
191 | this.timeout(15000) | ||
192 | |||
193 | const videoChannelAttributes = { | ||
194 | displayName: 'video channel updated', | ||
195 | description: 'video channel description updated', | ||
196 | support: 'support updated' | ||
197 | } | ||
198 | |||
199 | await servers[0].channels.update({ channelName: 'second_video_channel', attributes: videoChannelAttributes }) | ||
200 | |||
201 | await waitJobs(servers) | ||
202 | }) | ||
203 | |||
204 | it('Should have video channel updated', async function () { | ||
205 | for (const server of servers) { | ||
206 | const body = await server.channels.list({ start: 0, count: 1, sort: '-name' }) | ||
207 | |||
208 | expect(body.total).to.equal(2) | ||
209 | expect(body.data).to.be.an('array') | ||
210 | expect(body.data).to.have.lengthOf(1) | ||
211 | |||
212 | expect(body.data[0].name).to.equal('second_video_channel') | ||
213 | expect(body.data[0].displayName).to.equal('video channel updated') | ||
214 | expect(body.data[0].description).to.equal('video channel description updated') | ||
215 | expect(body.data[0].support).to.equal('support updated') | ||
216 | } | ||
217 | }) | ||
218 | |||
219 | it('Should not have updated the video support field', async function () { | ||
220 | for (const server of servers) { | ||
221 | const video = await server.videos.get({ id: videoUUID }) | ||
222 | expect(video.support).to.equal('video support field') | ||
223 | } | ||
224 | }) | ||
225 | |||
226 | it('Should update another accounts video channel', async function () { | ||
227 | this.timeout(15000) | ||
228 | |||
229 | const result = await servers[0].users.generate('second_user') | ||
230 | secondUserChannelName = result.userChannelName | ||
231 | |||
232 | await servers[0].videos.quickUpload({ name: 'video', token: result.token }) | ||
233 | |||
234 | const videoChannelAttributes = { | ||
235 | displayName: 'video channel updated', | ||
236 | description: 'video channel description updated', | ||
237 | support: 'support updated' | ||
238 | } | ||
239 | |||
240 | await servers[0].channels.update({ channelName: secondUserChannelName, attributes: videoChannelAttributes }) | ||
241 | |||
242 | await waitJobs(servers) | ||
243 | }) | ||
244 | |||
245 | it('Should have another accounts video channel updated', async function () { | ||
246 | for (const server of servers) { | ||
247 | const body = await server.channels.get({ channelName: `${secondUserChannelName}@${servers[0].host}` }) | ||
248 | |||
249 | expect(body.displayName).to.equal('video channel updated') | ||
250 | expect(body.description).to.equal('video channel description updated') | ||
251 | expect(body.support).to.equal('support updated') | ||
252 | } | ||
253 | }) | ||
254 | |||
255 | it('Should update the channel support field and update videos too', async function () { | ||
256 | this.timeout(35000) | ||
257 | |||
258 | const videoChannelAttributes = { | ||
259 | support: 'video channel support text updated', | ||
260 | bulkVideosSupportUpdate: true | ||
261 | } | ||
262 | |||
263 | await servers[0].channels.update({ channelName: 'second_video_channel', attributes: videoChannelAttributes }) | ||
264 | |||
265 | await waitJobs(servers) | ||
266 | |||
267 | for (const server of servers) { | ||
268 | const video = await server.videos.get({ id: videoUUID }) | ||
269 | expect(video.support).to.equal(videoChannelAttributes.support) | ||
270 | } | ||
271 | }) | ||
272 | |||
273 | it('Should update video channel avatar', async function () { | ||
274 | this.timeout(15000) | ||
275 | |||
276 | const fixture = 'avatar.png' | ||
277 | |||
278 | await servers[0].channels.updateImage({ | ||
279 | channelName: 'second_video_channel', | ||
280 | fixture, | ||
281 | type: 'avatar' | ||
282 | }) | ||
283 | |||
284 | await waitJobs(servers) | ||
285 | |||
286 | for (let i = 0; i < servers.length; i++) { | ||
287 | const server = servers[i] | ||
288 | |||
289 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
290 | const expectedSizes = ACTOR_IMAGES_SIZE[ActorImageType.AVATAR] | ||
291 | |||
292 | expect(videoChannel.avatars.length).to.equal(expectedSizes.length, 'Expected avatars to be generated in all sizes') | ||
293 | |||
294 | for (const avatar of videoChannel.avatars) { | ||
295 | avatarPaths[server.port] = avatar.path | ||
296 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatarPaths[server.port], '.png') | ||
297 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true) | ||
298 | |||
299 | const row = await sqlCommands[i].getActorImage(basename(avatarPaths[server.port])) | ||
300 | |||
301 | expect(expectedSizes.some(({ height, width }) => row.height === height && row.width === width)).to.equal(true) | ||
302 | } | ||
303 | } | ||
304 | }) | ||
305 | |||
306 | it('Should update video channel banner', async function () { | ||
307 | this.timeout(15000) | ||
308 | |||
309 | const fixture = 'banner.jpg' | ||
310 | |||
311 | await servers[0].channels.updateImage({ | ||
312 | channelName: 'second_video_channel', | ||
313 | fixture, | ||
314 | type: 'banner' | ||
315 | }) | ||
316 | |||
317 | await waitJobs(servers) | ||
318 | |||
319 | for (let i = 0; i < servers.length; i++) { | ||
320 | const server = servers[i] | ||
321 | |||
322 | const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host }) | ||
323 | |||
324 | bannerPaths[server.port] = videoChannel.banners[0].path | ||
325 | await testImage(server.url, 'banner-resized', bannerPaths[server.port]) | ||
326 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true) | ||
327 | |||
328 | const row = await sqlCommands[i].getActorImage(basename(bannerPaths[server.port])) | ||
329 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].height) | ||
330 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].width) | ||
331 | } | ||
332 | }) | ||
333 | |||
334 | it('Should still correctly list channels', async function () { | ||
335 | { | ||
336 | const body = await servers[0].channels.list({ start: 1, count: 1, sort: 'createdAt' }) | ||
337 | |||
338 | expect(body.total).to.equal(3) | ||
339 | expect(body.data).to.have.lengthOf(1) | ||
340 | expect(body.data[0].name).to.equal('second_video_channel') | ||
341 | } | ||
342 | |||
343 | { | ||
344 | const body = await servers[0].channels.listByAccount({ accountName, start: 1, count: 1, sort: 'createdAt' }) | ||
345 | |||
346 | expect(body.total).to.equal(2) | ||
347 | expect(body.data).to.have.lengthOf(1) | ||
348 | expect(body.data[0].name).to.equal('second_video_channel') | ||
349 | } | ||
350 | }) | ||
351 | |||
352 | it('Should delete the video channel avatar', async function () { | ||
353 | this.timeout(15000) | ||
354 | await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'avatar' }) | ||
355 | |||
356 | await waitJobs(servers) | ||
357 | |||
358 | for (const server of servers) { | ||
359 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
360 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false) | ||
361 | |||
362 | expect(videoChannel.avatars).to.be.empty | ||
363 | } | ||
364 | }) | ||
365 | |||
366 | it('Should delete the video channel banner', async function () { | ||
367 | this.timeout(15000) | ||
368 | |||
369 | await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'banner' }) | ||
370 | |||
371 | await waitJobs(servers) | ||
372 | |||
373 | for (const server of servers) { | ||
374 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
375 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false) | ||
376 | |||
377 | expect(videoChannel.banners).to.be.empty | ||
378 | } | ||
379 | }) | ||
380 | |||
381 | it('Should list the second video channel videos', async function () { | ||
382 | for (const server of servers) { | ||
383 | const channelURI = 'second_video_channel@' + servers[0].host | ||
384 | const { total, data } = await server.videos.listByChannel({ handle: channelURI }) | ||
385 | |||
386 | expect(total).to.equal(1) | ||
387 | expect(data).to.be.an('array') | ||
388 | expect(data).to.have.lengthOf(1) | ||
389 | expect(data[0].name).to.equal('my video name') | ||
390 | } | ||
391 | }) | ||
392 | |||
393 | it('Should change the video channel of a video', async function () { | ||
394 | await servers[0].videos.update({ id: videoUUID, attributes: { channelId: servers[0].store.channel.id } }) | ||
395 | |||
396 | await waitJobs(servers) | ||
397 | }) | ||
398 | |||
399 | it('Should list the first video channel videos', async function () { | ||
400 | for (const server of servers) { | ||
401 | { | ||
402 | const secondChannelURI = 'second_video_channel@' + servers[0].host | ||
403 | const { total } = await server.videos.listByChannel({ handle: secondChannelURI }) | ||
404 | expect(total).to.equal(0) | ||
405 | } | ||
406 | |||
407 | { | ||
408 | const channelURI = 'root_channel@' + servers[0].host | ||
409 | const { total, data } = await server.videos.listByChannel({ handle: channelURI }) | ||
410 | expect(total).to.equal(1) | ||
411 | |||
412 | expect(data).to.be.an('array') | ||
413 | expect(data).to.have.lengthOf(1) | ||
414 | expect(data[0].name).to.equal('my video name') | ||
415 | } | ||
416 | } | ||
417 | }) | ||
418 | |||
419 | it('Should delete video channel', async function () { | ||
420 | await servers[0].channels.delete({ channelName: 'second_video_channel' }) | ||
421 | }) | ||
422 | |||
423 | it('Should have video channel deleted', async function () { | ||
424 | const body = await servers[0].channels.list({ start: 0, count: 10, sort: 'createdAt' }) | ||
425 | |||
426 | expect(body.total).to.equal(2) | ||
427 | expect(body.data).to.be.an('array') | ||
428 | expect(body.data).to.have.lengthOf(2) | ||
429 | expect(body.data[0].displayName).to.equal('Main root channel') | ||
430 | expect(body.data[1].displayName).to.equal('video channel updated') | ||
431 | }) | ||
432 | |||
433 | it('Should create the main channel with a suffix if there is a conflict', async function () { | ||
434 | { | ||
435 | const videoChannel = { name: 'toto_channel', displayName: 'My toto channel' } | ||
436 | const created = await servers[0].channels.create({ attributes: videoChannel }) | ||
437 | totoChannel = created.id | ||
438 | } | ||
439 | |||
440 | { | ||
441 | await servers[0].users.create({ username: 'toto', password: 'password' }) | ||
442 | const accessToken = await servers[0].login.getAccessToken({ username: 'toto', password: 'password' }) | ||
443 | |||
444 | const { videoChannels } = await servers[0].users.getMyInfo({ token: accessToken }) | ||
445 | expect(videoChannels[0].name).to.equal('toto_channel-1') | ||
446 | } | ||
447 | }) | ||
448 | |||
449 | it('Should report correct channel views per days', async function () { | ||
450 | { | ||
451 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) | ||
452 | |||
453 | for (const channel of data) { | ||
454 | expect(channel).to.haveOwnProperty('viewsPerDay') | ||
455 | expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today | ||
456 | |||
457 | for (const v of channel.viewsPerDay) { | ||
458 | expect(v.date).to.be.an('string') | ||
459 | expect(v.views).to.equal(0) | ||
460 | } | ||
461 | } | ||
462 | } | ||
463 | |||
464 | { | ||
465 | // video has been posted on channel servers[0].store.videoChannel.id since last update | ||
466 | await servers[0].views.simulateView({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1' }) | ||
467 | await servers[0].views.simulateView({ id: videoUUID, xForwardedFor: '0.0.0.2,127.0.0.1' }) | ||
468 | |||
469 | // Wait the repeatable job | ||
470 | await wait(8000) | ||
471 | |||
472 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) | ||
473 | const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id) | ||
474 | expect(channelWithView.viewsPerDay.slice(-1)[0].views).to.equal(2) | ||
475 | } | ||
476 | }) | ||
477 | |||
478 | it('Should report correct total views count', async function () { | ||
479 | // check if there's the property | ||
480 | { | ||
481 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) | ||
482 | |||
483 | for (const channel of data) { | ||
484 | expect(channel).to.haveOwnProperty('totalViews') | ||
485 | expect(channel.totalViews).to.be.a('number') | ||
486 | } | ||
487 | } | ||
488 | |||
489 | // Check if the totalViews count can be updated | ||
490 | { | ||
491 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) | ||
492 | const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id) | ||
493 | expect(channelWithView.totalViews).to.equal(2) | ||
494 | } | ||
495 | }) | ||
496 | |||
497 | it('Should report correct videos count', async function () { | ||
498 | const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) | ||
499 | |||
500 | const totoChannel = data.find(c => c.name === 'toto_channel') | ||
501 | const rootChannel = data.find(c => c.name === 'root_channel') | ||
502 | |||
503 | expect(rootChannel.videosCount).to.equal(1) | ||
504 | expect(totoChannel.videosCount).to.equal(0) | ||
505 | }) | ||
506 | |||
507 | it('Should search among account video channels', async function () { | ||
508 | { | ||
509 | const body = await servers[0].channels.listByAccount({ accountName, search: 'root' }) | ||
510 | expect(body.total).to.equal(1) | ||
511 | |||
512 | const channels = body.data | ||
513 | expect(channels).to.have.lengthOf(1) | ||
514 | } | ||
515 | |||
516 | { | ||
517 | const body = await servers[0].channels.listByAccount({ accountName, search: 'does not exist' }) | ||
518 | expect(body.total).to.equal(0) | ||
519 | |||
520 | const channels = body.data | ||
521 | expect(channels).to.have.lengthOf(0) | ||
522 | } | ||
523 | }) | ||
524 | |||
525 | it('Should list channels by updatedAt desc if a video has been uploaded', async function () { | ||
526 | this.timeout(30000) | ||
527 | |||
528 | await servers[0].videos.upload({ attributes: { channelId: totoChannel } }) | ||
529 | await waitJobs(servers) | ||
530 | |||
531 | for (const server of servers) { | ||
532 | const { data } = await server.channels.listByAccount({ accountName, sort: '-updatedAt' }) | ||
533 | |||
534 | expect(data[0].name).to.equal('toto_channel') | ||
535 | expect(data[1].name).to.equal('root_channel') | ||
536 | } | ||
537 | |||
538 | await servers[0].videos.upload({ attributes: { channelId: servers[0].store.channel.id } }) | ||
539 | await waitJobs(servers) | ||
540 | |||
541 | for (const server of servers) { | ||
542 | const { data } = await server.channels.listByAccount({ accountName, sort: '-updatedAt' }) | ||
543 | |||
544 | expect(data[0].name).to.equal('root_channel') | ||
545 | expect(data[1].name).to.equal('toto_channel') | ||
546 | } | ||
547 | }) | ||
548 | |||
549 | after(async function () { | ||
550 | for (const sqlCommand of sqlCommands) { | ||
551 | await sqlCommand.cleanup() | ||
552 | } | ||
553 | |||
554 | await cleanupTests(servers) | ||
555 | }) | ||
556 | }) | ||
diff --git a/packages/tests/src/api/videos/video-comments.ts b/packages/tests/src/api/videos/video-comments.ts new file mode 100644 index 000000000..f17db9979 --- /dev/null +++ b/packages/tests/src/api/videos/video-comments.ts | |||
@@ -0,0 +1,335 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { dateIsValid, testImage } from '@tests/shared/checks.js' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | CommentsCommand, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultAccountAvatar, | ||
12 | setDefaultChannelAvatar | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test video comments', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let videoId: number | ||
18 | let videoUUID: string | ||
19 | let threadId: number | ||
20 | let replyToDeleteId: number | ||
21 | |||
22 | let userAccessTokenServer1: string | ||
23 | |||
24 | let command: CommentsCommand | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(120000) | ||
28 | |||
29 | server = await createSingleServer(1) | ||
30 | |||
31 | await setAccessTokensToServers([ server ]) | ||
32 | |||
33 | const { id, uuid } = await server.videos.upload() | ||
34 | videoUUID = uuid | ||
35 | videoId = id | ||
36 | |||
37 | await setDefaultChannelAvatar(server) | ||
38 | await setDefaultAccountAvatar(server) | ||
39 | |||
40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | ||
41 | await setDefaultChannelAvatar(server, 'user1_channel') | ||
42 | await setDefaultAccountAvatar(server, userAccessTokenServer1) | ||
43 | |||
44 | command = server.comments | ||
45 | }) | ||
46 | |||
47 | describe('User comments', function () { | ||
48 | |||
49 | it('Should not have threads on this video', async function () { | ||
50 | const body = await command.listThreads({ videoId: videoUUID }) | ||
51 | |||
52 | expect(body.total).to.equal(0) | ||
53 | expect(body.totalNotDeletedComments).to.equal(0) | ||
54 | expect(body.data).to.be.an('array') | ||
55 | expect(body.data).to.have.lengthOf(0) | ||
56 | }) | ||
57 | |||
58 | it('Should create a thread in this video', async function () { | ||
59 | const text = 'my super first comment' | ||
60 | |||
61 | const comment = await command.createThread({ videoId: videoUUID, text }) | ||
62 | |||
63 | expect(comment.inReplyToCommentId).to.be.null | ||
64 | expect(comment.text).equal('my super first comment') | ||
65 | expect(comment.videoId).to.equal(videoId) | ||
66 | expect(comment.id).to.equal(comment.threadId) | ||
67 | expect(comment.account.name).to.equal('root') | ||
68 | expect(comment.account.host).to.equal(server.host) | ||
69 | expect(comment.account.url).to.equal(server.url + '/accounts/root') | ||
70 | expect(comment.totalReplies).to.equal(0) | ||
71 | expect(comment.totalRepliesFromVideoAuthor).to.equal(0) | ||
72 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
73 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
74 | }) | ||
75 | |||
76 | it('Should list threads of this video', async function () { | ||
77 | const body = await command.listThreads({ videoId: videoUUID }) | ||
78 | |||
79 | expect(body.total).to.equal(1) | ||
80 | expect(body.totalNotDeletedComments).to.equal(1) | ||
81 | expect(body.data).to.be.an('array') | ||
82 | expect(body.data).to.have.lengthOf(1) | ||
83 | |||
84 | const comment = body.data[0] | ||
85 | expect(comment.inReplyToCommentId).to.be.null | ||
86 | expect(comment.text).equal('my super first comment') | ||
87 | expect(comment.videoId).to.equal(videoId) | ||
88 | expect(comment.id).to.equal(comment.threadId) | ||
89 | expect(comment.account.name).to.equal('root') | ||
90 | expect(comment.account.host).to.equal(server.host) | ||
91 | |||
92 | for (const avatar of comment.account.avatars) { | ||
93 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
94 | } | ||
95 | |||
96 | expect(comment.totalReplies).to.equal(0) | ||
97 | expect(comment.totalRepliesFromVideoAuthor).to.equal(0) | ||
98 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
99 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
100 | |||
101 | threadId = comment.threadId | ||
102 | }) | ||
103 | |||
104 | it('Should get all the thread created', async function () { | ||
105 | const body = await command.getThread({ videoId: videoUUID, threadId }) | ||
106 | |||
107 | const rootComment = body.comment | ||
108 | expect(rootComment.inReplyToCommentId).to.be.null | ||
109 | expect(rootComment.text).equal('my super first comment') | ||
110 | expect(rootComment.videoId).to.equal(videoId) | ||
111 | expect(dateIsValid(rootComment.createdAt as string)).to.be.true | ||
112 | expect(dateIsValid(rootComment.updatedAt as string)).to.be.true | ||
113 | }) | ||
114 | |||
115 | it('Should create multiple replies in this thread', async function () { | ||
116 | const text1 = 'my super answer to thread 1' | ||
117 | const created = await command.addReply({ videoId, toCommentId: threadId, text: text1 }) | ||
118 | const childCommentId = created.id | ||
119 | |||
120 | const text2 = 'my super answer to answer of thread 1' | ||
121 | await command.addReply({ videoId, toCommentId: childCommentId, text: text2 }) | ||
122 | |||
123 | const text3 = 'my second answer to thread 1' | ||
124 | await command.addReply({ videoId, toCommentId: threadId, text: text3 }) | ||
125 | }) | ||
126 | |||
127 | it('Should get correctly the replies', async function () { | ||
128 | const tree = await command.getThread({ videoId: videoUUID, threadId }) | ||
129 | |||
130 | expect(tree.comment.text).equal('my super first comment') | ||
131 | expect(tree.children).to.have.lengthOf(2) | ||
132 | |||
133 | const firstChild = tree.children[0] | ||
134 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
135 | expect(firstChild.children).to.have.lengthOf(1) | ||
136 | |||
137 | const childOfFirstChild = firstChild.children[0] | ||
138 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
139 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
140 | |||
141 | const secondChild = tree.children[1] | ||
142 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | ||
143 | expect(secondChild.children).to.have.lengthOf(0) | ||
144 | |||
145 | replyToDeleteId = secondChild.comment.id | ||
146 | }) | ||
147 | |||
148 | it('Should create other threads', async function () { | ||
149 | const text1 = 'super thread 2' | ||
150 | await command.createThread({ videoId: videoUUID, text: text1 }) | ||
151 | |||
152 | const text2 = 'super thread 3' | ||
153 | await command.createThread({ videoId: videoUUID, text: text2 }) | ||
154 | }) | ||
155 | |||
156 | it('Should list the threads', async function () { | ||
157 | const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' }) | ||
158 | |||
159 | expect(body.total).to.equal(3) | ||
160 | expect(body.totalNotDeletedComments).to.equal(6) | ||
161 | expect(body.data).to.be.an('array') | ||
162 | expect(body.data).to.have.lengthOf(3) | ||
163 | |||
164 | expect(body.data[0].text).to.equal('my super first comment') | ||
165 | expect(body.data[0].totalReplies).to.equal(3) | ||
166 | expect(body.data[1].text).to.equal('super thread 2') | ||
167 | expect(body.data[1].totalReplies).to.equal(0) | ||
168 | expect(body.data[2].text).to.equal('super thread 3') | ||
169 | expect(body.data[2].totalReplies).to.equal(0) | ||
170 | }) | ||
171 | |||
172 | it('Should list the and sort them by total replies', async function () { | ||
173 | const body = await command.listThreads({ videoId: videoUUID, sort: 'totalReplies' }) | ||
174 | |||
175 | expect(body.data[2].text).to.equal('my super first comment') | ||
176 | expect(body.data[2].totalReplies).to.equal(3) | ||
177 | }) | ||
178 | |||
179 | it('Should delete a reply', async function () { | ||
180 | await command.delete({ videoId, commentId: replyToDeleteId }) | ||
181 | |||
182 | { | ||
183 | const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' }) | ||
184 | |||
185 | expect(body.total).to.equal(3) | ||
186 | expect(body.totalNotDeletedComments).to.equal(5) | ||
187 | } | ||
188 | |||
189 | { | ||
190 | const tree = await command.getThread({ videoId: videoUUID, threadId }) | ||
191 | |||
192 | expect(tree.comment.text).equal('my super first comment') | ||
193 | expect(tree.children).to.have.lengthOf(2) | ||
194 | |||
195 | const firstChild = tree.children[0] | ||
196 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
197 | expect(firstChild.children).to.have.lengthOf(1) | ||
198 | |||
199 | const childOfFirstChild = firstChild.children[0] | ||
200 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
201 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
202 | |||
203 | const deletedChildOfFirstChild = tree.children[1] | ||
204 | expect(deletedChildOfFirstChild.comment.text).to.equal('') | ||
205 | expect(deletedChildOfFirstChild.comment.isDeleted).to.be.true | ||
206 | expect(deletedChildOfFirstChild.comment.deletedAt).to.not.be.null | ||
207 | expect(deletedChildOfFirstChild.comment.account).to.be.null | ||
208 | expect(deletedChildOfFirstChild.children).to.have.lengthOf(0) | ||
209 | } | ||
210 | }) | ||
211 | |||
212 | it('Should delete a complete thread', async function () { | ||
213 | await command.delete({ videoId, commentId: threadId }) | ||
214 | |||
215 | const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' }) | ||
216 | expect(body.total).to.equal(3) | ||
217 | expect(body.data).to.be.an('array') | ||
218 | expect(body.data).to.have.lengthOf(3) | ||
219 | |||
220 | expect(body.data[0].text).to.equal('') | ||
221 | expect(body.data[0].isDeleted).to.be.true | ||
222 | expect(body.data[0].deletedAt).to.not.be.null | ||
223 | expect(body.data[0].account).to.be.null | ||
224 | expect(body.data[0].totalReplies).to.equal(2) | ||
225 | expect(body.data[1].text).to.equal('super thread 2') | ||
226 | expect(body.data[1].totalReplies).to.equal(0) | ||
227 | expect(body.data[2].text).to.equal('super thread 3') | ||
228 | expect(body.data[2].totalReplies).to.equal(0) | ||
229 | }) | ||
230 | |||
231 | it('Should count replies from the video author correctly', async function () { | ||
232 | await command.createThread({ videoId: videoUUID, text: 'my super first comment' }) | ||
233 | |||
234 | const { data } = await command.listThreads({ videoId: videoUUID }) | ||
235 | const threadId2 = data[0].threadId | ||
236 | |||
237 | const text2 = 'a first answer to thread 4 by a third party' | ||
238 | await command.addReply({ token: userAccessTokenServer1, videoId, toCommentId: threadId2, text: text2 }) | ||
239 | |||
240 | const text3 = 'my second answer to thread 4' | ||
241 | await command.addReply({ videoId, toCommentId: threadId2, text: text3 }) | ||
242 | |||
243 | const tree = await command.getThread({ videoId: videoUUID, threadId: threadId2 }) | ||
244 | expect(tree.comment.totalRepliesFromVideoAuthor).to.equal(1) | ||
245 | expect(tree.comment.totalReplies).to.equal(2) | ||
246 | }) | ||
247 | }) | ||
248 | |||
249 | describe('All instance comments', function () { | ||
250 | |||
251 | it('Should list instance comments as admin', async function () { | ||
252 | { | ||
253 | const { data, total } = await command.listForAdmin({ start: 0, count: 1 }) | ||
254 | |||
255 | expect(total).to.equal(7) | ||
256 | expect(data).to.have.lengthOf(1) | ||
257 | expect(data[0].text).to.equal('my second answer to thread 4') | ||
258 | expect(data[0].account.name).to.equal('root') | ||
259 | expect(data[0].account.displayName).to.equal('root') | ||
260 | expect(data[0].account.avatars).to.have.lengthOf(2) | ||
261 | } | ||
262 | |||
263 | { | ||
264 | const { data, total } = await command.listForAdmin({ start: 1, count: 2 }) | ||
265 | |||
266 | expect(total).to.equal(7) | ||
267 | expect(data).to.have.lengthOf(2) | ||
268 | |||
269 | expect(data[0].account.avatars).to.have.lengthOf(2) | ||
270 | expect(data[1].account.avatars).to.have.lengthOf(2) | ||
271 | } | ||
272 | }) | ||
273 | |||
274 | it('Should filter instance comments by isLocal', async function () { | ||
275 | const { total, data } = await command.listForAdmin({ isLocal: false }) | ||
276 | |||
277 | expect(data).to.have.lengthOf(0) | ||
278 | expect(total).to.equal(0) | ||
279 | }) | ||
280 | |||
281 | it('Should filter instance comments by onLocalVideo', async function () { | ||
282 | { | ||
283 | const { total, data } = await command.listForAdmin({ onLocalVideo: false }) | ||
284 | |||
285 | expect(data).to.have.lengthOf(0) | ||
286 | expect(total).to.equal(0) | ||
287 | } | ||
288 | |||
289 | { | ||
290 | const { total, data } = await command.listForAdmin({ onLocalVideo: true }) | ||
291 | |||
292 | expect(data).to.not.have.lengthOf(0) | ||
293 | expect(total).to.not.equal(0) | ||
294 | } | ||
295 | }) | ||
296 | |||
297 | it('Should search instance comments by account', async function () { | ||
298 | const { total, data } = await command.listForAdmin({ searchAccount: 'user' }) | ||
299 | |||
300 | expect(data).to.have.lengthOf(1) | ||
301 | expect(total).to.equal(1) | ||
302 | |||
303 | expect(data[0].text).to.equal('a first answer to thread 4 by a third party') | ||
304 | }) | ||
305 | |||
306 | it('Should search instance comments by video', async function () { | ||
307 | { | ||
308 | const { total, data } = await command.listForAdmin({ searchVideo: 'video' }) | ||
309 | |||
310 | expect(data).to.have.lengthOf(7) | ||
311 | expect(total).to.equal(7) | ||
312 | } | ||
313 | |||
314 | { | ||
315 | const { total, data } = await command.listForAdmin({ searchVideo: 'hello' }) | ||
316 | |||
317 | expect(data).to.have.lengthOf(0) | ||
318 | expect(total).to.equal(0) | ||
319 | } | ||
320 | }) | ||
321 | |||
322 | it('Should search instance comments', async function () { | ||
323 | const { total, data } = await command.listForAdmin({ search: 'super thread 3' }) | ||
324 | |||
325 | expect(total).to.equal(1) | ||
326 | |||
327 | expect(data).to.have.lengthOf(1) | ||
328 | expect(data[0].text).to.equal('super thread 3') | ||
329 | }) | ||
330 | }) | ||
331 | |||
332 | after(async function () { | ||
333 | await cleanupTests([ server ]) | ||
334 | }) | ||
335 | }) | ||
diff --git a/packages/tests/src/api/videos/video-description.ts b/packages/tests/src/api/videos/video-description.ts new file mode 100644 index 000000000..eb41cd71c --- /dev/null +++ b/packages/tests/src/api/videos/video-description.ts | |||
@@ -0,0 +1,103 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | waitJobs | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test video description', function () { | ||
14 | let servers: PeerTubeServer[] = [] | ||
15 | let videoUUID = '' | ||
16 | let videoId: number | ||
17 | |||
18 | const longDescription = 'my super description for server 1'.repeat(50) | ||
19 | |||
20 | // 30 characters * 6 -> 240 characters | ||
21 | const truncatedDescription = 'my super description for server 1'.repeat(7) + 'my super descrip...' | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(40000) | ||
25 | |||
26 | // Run servers | ||
27 | servers = await createMultipleServers(2) | ||
28 | |||
29 | // Get the access tokens | ||
30 | await setAccessTokensToServers(servers) | ||
31 | |||
32 | // Server 1 and server 2 follow each other | ||
33 | await doubleFollow(servers[0], servers[1]) | ||
34 | }) | ||
35 | |||
36 | it('Should upload video with long description', async function () { | ||
37 | this.timeout(30000) | ||
38 | |||
39 | const attributes = { | ||
40 | description: longDescription | ||
41 | } | ||
42 | await servers[0].videos.upload({ attributes }) | ||
43 | |||
44 | await waitJobs(servers) | ||
45 | |||
46 | const { data } = await servers[0].videos.list() | ||
47 | |||
48 | videoId = data[0].id | ||
49 | videoUUID = data[0].uuid | ||
50 | }) | ||
51 | |||
52 | it('Should have a truncated description on each server when listing videos', async function () { | ||
53 | for (const server of servers) { | ||
54 | const { data } = await server.videos.list() | ||
55 | const video = data.find(v => v.uuid === videoUUID) | ||
56 | |||
57 | expect(video.description).to.equal(truncatedDescription) | ||
58 | expect(video.truncatedDescription).to.equal(truncatedDescription) | ||
59 | } | ||
60 | }) | ||
61 | |||
62 | it('Should not have a truncated description on each server when getting videos', async function () { | ||
63 | for (const server of servers) { | ||
64 | const video = await server.videos.get({ id: videoUUID }) | ||
65 | |||
66 | expect(video.description).to.equal(longDescription) | ||
67 | expect(video.truncatedDescription).to.equal(truncatedDescription) | ||
68 | } | ||
69 | }) | ||
70 | |||
71 | it('Should fetch long description on each server', async function () { | ||
72 | for (const server of servers) { | ||
73 | const video = await server.videos.get({ id: videoUUID }) | ||
74 | |||
75 | const { description } = await server.videos.getDescription({ descriptionPath: video.descriptionPath }) | ||
76 | expect(description).to.equal(longDescription) | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | it('Should update with a short description', async function () { | ||
81 | const attributes = { | ||
82 | description: 'short description' | ||
83 | } | ||
84 | await servers[0].videos.update({ id: videoId, attributes }) | ||
85 | |||
86 | await waitJobs(servers) | ||
87 | }) | ||
88 | |||
89 | it('Should have a small description on each server', async function () { | ||
90 | for (const server of servers) { | ||
91 | const video = await server.videos.get({ id: videoUUID }) | ||
92 | |||
93 | expect(video.description).to.equal('short description') | ||
94 | |||
95 | const { description } = await server.videos.getDescription({ descriptionPath: video.descriptionPath }) | ||
96 | expect(description).to.equal('short description') | ||
97 | } | ||
98 | }) | ||
99 | |||
100 | after(async function () { | ||
101 | await cleanupTests(servers) | ||
102 | }) | ||
103 | }) | ||
diff --git a/packages/tests/src/api/videos/video-files.ts b/packages/tests/src/api/videos/video-files.ts new file mode 100644 index 000000000..1d7c218a4 --- /dev/null +++ b/packages/tests/src/api/videos/video-files.ts | |||
@@ -0,0 +1,202 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | makeRawRequest, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test videos files', function () { | ||
16 | let servers: PeerTubeServer[] | ||
17 | |||
18 | // --------------------------------------------------------------- | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(150_000) | ||
22 | |||
23 | servers = await createMultipleServers(2) | ||
24 | await setAccessTokensToServers(servers) | ||
25 | |||
26 | await doubleFollow(servers[0], servers[1]) | ||
27 | |||
28 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) | ||
29 | }) | ||
30 | |||
31 | describe('When deleting all files', function () { | ||
32 | let validId1: string | ||
33 | let validId2: string | ||
34 | |||
35 | before(async function () { | ||
36 | this.timeout(360_000) | ||
37 | |||
38 | { | ||
39 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' }) | ||
40 | validId1 = uuid | ||
41 | } | ||
42 | |||
43 | { | ||
44 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 2' }) | ||
45 | validId2 = uuid | ||
46 | } | ||
47 | |||
48 | await waitJobs(servers) | ||
49 | }) | ||
50 | |||
51 | it('Should delete web video files', async function () { | ||
52 | this.timeout(30_000) | ||
53 | |||
54 | await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1 }) | ||
55 | |||
56 | await waitJobs(servers) | ||
57 | |||
58 | for (const server of servers) { | ||
59 | const video = await server.videos.get({ id: validId1 }) | ||
60 | |||
61 | expect(video.files).to.have.lengthOf(0) | ||
62 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
63 | } | ||
64 | }) | ||
65 | |||
66 | it('Should delete HLS files', async function () { | ||
67 | this.timeout(30_000) | ||
68 | |||
69 | await servers[0].videos.removeHLSPlaylist({ videoId: validId2 }) | ||
70 | |||
71 | await waitJobs(servers) | ||
72 | |||
73 | for (const server of servers) { | ||
74 | const video = await server.videos.get({ id: validId2 }) | ||
75 | |||
76 | expect(video.files).to.have.length.above(0) | ||
77 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
78 | } | ||
79 | }) | ||
80 | }) | ||
81 | |||
82 | describe('When deleting a specific file', function () { | ||
83 | let webVideoId: string | ||
84 | let hlsId: string | ||
85 | |||
86 | before(async function () { | ||
87 | this.timeout(120_000) | ||
88 | |||
89 | { | ||
90 | const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' }) | ||
91 | webVideoId = uuid | ||
92 | } | ||
93 | |||
94 | { | ||
95 | const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' }) | ||
96 | hlsId = uuid | ||
97 | } | ||
98 | |||
99 | await waitJobs(servers) | ||
100 | }) | ||
101 | |||
102 | it('Shoulde delete a web video file', async function () { | ||
103 | this.timeout(30_000) | ||
104 | |||
105 | const video = await servers[0].videos.get({ id: webVideoId }) | ||
106 | const files = video.files | ||
107 | |||
108 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: files[0].id }) | ||
109 | |||
110 | await waitJobs(servers) | ||
111 | |||
112 | for (const server of servers) { | ||
113 | const video = await server.videos.get({ id: webVideoId }) | ||
114 | |||
115 | expect(video.files).to.have.lengthOf(files.length - 1) | ||
116 | expect(video.files.find(f => f.id === files[0].id)).to.not.exist | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | it('Should delete all web video files', async function () { | ||
121 | this.timeout(30_000) | ||
122 | |||
123 | const video = await servers[0].videos.get({ id: webVideoId }) | ||
124 | const files = video.files | ||
125 | |||
126 | for (const file of files) { | ||
127 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: file.id }) | ||
128 | } | ||
129 | |||
130 | await waitJobs(servers) | ||
131 | |||
132 | for (const server of servers) { | ||
133 | const video = await server.videos.get({ id: webVideoId }) | ||
134 | |||
135 | expect(video.files).to.have.lengthOf(0) | ||
136 | } | ||
137 | }) | ||
138 | |||
139 | it('Should delete a hls file', async function () { | ||
140 | this.timeout(30_000) | ||
141 | |||
142 | const video = await servers[0].videos.get({ id: hlsId }) | ||
143 | const files = video.streamingPlaylists[0].files | ||
144 | const toDelete = files[0] | ||
145 | |||
146 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: toDelete.id }) | ||
147 | |||
148 | await waitJobs(servers) | ||
149 | |||
150 | for (const server of servers) { | ||
151 | const video = await server.videos.get({ id: hlsId }) | ||
152 | |||
153 | expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1) | ||
154 | expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist | ||
155 | |||
156 | const { text } = await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
157 | |||
158 | expect(text.includes(`-${toDelete.resolution.id}.m3u8`)).to.be.false | ||
159 | expect(text.includes(`-${video.streamingPlaylists[0].files[0].resolution.id}.m3u8`)).to.be.true | ||
160 | } | ||
161 | }) | ||
162 | |||
163 | it('Should delete all hls files', async function () { | ||
164 | this.timeout(30_000) | ||
165 | |||
166 | const video = await servers[0].videos.get({ id: hlsId }) | ||
167 | const files = video.streamingPlaylists[0].files | ||
168 | |||
169 | for (const file of files) { | ||
170 | await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: file.id }) | ||
171 | } | ||
172 | |||
173 | await waitJobs(servers) | ||
174 | |||
175 | for (const server of servers) { | ||
176 | const video = await server.videos.get({ id: hlsId }) | ||
177 | |||
178 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
179 | } | ||
180 | }) | ||
181 | |||
182 | it('Should not delete last file of a video', async function () { | ||
183 | this.timeout(60_000) | ||
184 | |||
185 | const webVideoOnly = await servers[0].videos.get({ id: hlsId }) | ||
186 | const hlsOnly = await servers[0].videos.get({ id: webVideoId }) | ||
187 | |||
188 | for (let i = 0; i < 4; i++) { | ||
189 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[i].id }) | ||
190 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id }) | ||
191 | } | ||
192 | |||
193 | const expectedStatus = HttpStatusCode.BAD_REQUEST_400 | ||
194 | await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[4].id, expectedStatus }) | ||
195 | await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus }) | ||
196 | }) | ||
197 | }) | ||
198 | |||
199 | after(async function () { | ||
200 | await cleanupTests(servers) | ||
201 | }) | ||
202 | }) | ||
diff --git a/packages/tests/src/api/videos/video-imports.ts b/packages/tests/src/api/videos/video-imports.ts new file mode 100644 index 000000000..09efe9931 --- /dev/null +++ b/packages/tests/src/api/videos/video-imports.ts | |||
@@ -0,0 +1,634 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists, remove } from 'fs-extra/esm' | ||
5 | import { readdir } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
8 | import { CustomConfig, HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@peertube/peertube-models' | ||
9 | import { | ||
10 | cleanupTests, | ||
11 | createMultipleServers, | ||
12 | createSingleServer, | ||
13 | doubleFollow, | ||
14 | getServerImportConfig, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel, | ||
18 | waitJobs | ||
19 | } from '@peertube/peertube-server-commands' | ||
20 | import { DeepPartial } from '@peertube/peertube-typescript-utils' | ||
21 | import { testCaptionFile } from '@tests/shared/captions.js' | ||
22 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
23 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
24 | |||
25 | async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMagnet: string, idTorrent: string) { | ||
26 | const videoHttp = await server.videos.get({ id: idHttp }) | ||
27 | |||
28 | expect(videoHttp.name).to.equal('small video - youtube') | ||
29 | expect(videoHttp.category.label).to.equal('News & Politics') | ||
30 | expect(videoHttp.licence.label).to.equal('Attribution') | ||
31 | expect(videoHttp.language.label).to.equal('Unknown') | ||
32 | expect(videoHttp.nsfw).to.be.false | ||
33 | expect(videoHttp.description).to.equal('this is a super description') | ||
34 | expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ]) | ||
35 | expect(videoHttp.files).to.have.lengthOf(1) | ||
36 | |||
37 | const originallyPublishedAt = new Date(videoHttp.originallyPublishedAt) | ||
38 | expect(originallyPublishedAt.getDate()).to.equal(14) | ||
39 | expect(originallyPublishedAt.getMonth()).to.equal(0) | ||
40 | expect(originallyPublishedAt.getFullYear()).to.equal(2019) | ||
41 | |||
42 | const videoMagnet = await server.videos.get({ id: idMagnet }) | ||
43 | const videoTorrent = await server.videos.get({ id: idTorrent }) | ||
44 | |||
45 | for (const video of [ videoMagnet, videoTorrent ]) { | ||
46 | expect(video.category.label).to.equal('Unknown') | ||
47 | expect(video.licence.label).to.equal('Unknown') | ||
48 | expect(video.language.label).to.equal('Unknown') | ||
49 | expect(video.nsfw).to.be.false | ||
50 | expect(video.description).to.equal('this is a super torrent description') | ||
51 | expect(video.tags).to.deep.equal([ 'tag_torrent1', 'tag_torrent2' ]) | ||
52 | expect(video.files).to.have.lengthOf(1) | ||
53 | } | ||
54 | |||
55 | expect(videoTorrent.name).to.contain('ä½ å¥½ 世界 720p.mp4') | ||
56 | expect(videoMagnet.name).to.contain('super peertube2 video') | ||
57 | |||
58 | const bodyCaptions = await server.captions.list({ videoId: idHttp }) | ||
59 | expect(bodyCaptions.total).to.equal(2) | ||
60 | } | ||
61 | |||
62 | async function checkVideoServer2 (server: PeerTubeServer, id: number | string) { | ||
63 | const video = await server.videos.get({ id }) | ||
64 | |||
65 | expect(video.name).to.equal('my super name') | ||
66 | expect(video.category.label).to.equal('Entertainment') | ||
67 | expect(video.licence.label).to.equal('Public Domain Dedication') | ||
68 | expect(video.language.label).to.equal('English') | ||
69 | expect(video.nsfw).to.be.false | ||
70 | expect(video.description).to.equal('my super description') | ||
71 | expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ]) | ||
72 | |||
73 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', video.thumbnailPath) | ||
74 | |||
75 | expect(video.files).to.have.lengthOf(1) | ||
76 | |||
77 | const bodyCaptions = await server.captions.list({ videoId: id }) | ||
78 | expect(bodyCaptions.total).to.equal(2) | ||
79 | } | ||
80 | |||
81 | describe('Test video imports', function () { | ||
82 | |||
83 | if (areHttpImportTestsDisabled()) return | ||
84 | |||
85 | function runSuite (mode: 'youtube-dl' | 'yt-dlp') { | ||
86 | |||
87 | describe('Import ' + mode, function () { | ||
88 | let servers: PeerTubeServer[] = [] | ||
89 | |||
90 | before(async function () { | ||
91 | this.timeout(60_000) | ||
92 | |||
93 | servers = await createMultipleServers(2, getServerImportConfig(mode)) | ||
94 | |||
95 | await setAccessTokensToServers(servers) | ||
96 | await setDefaultVideoChannel(servers) | ||
97 | |||
98 | for (const server of servers) { | ||
99 | await server.config.updateExistingSubConfig({ | ||
100 | newConfig: { | ||
101 | transcoding: { | ||
102 | alwaysTranscodeOriginalResolution: false | ||
103 | } | ||
104 | } | ||
105 | }) | ||
106 | } | ||
107 | |||
108 | await doubleFollow(servers[0], servers[1]) | ||
109 | }) | ||
110 | |||
111 | it('Should import videos on server 1', async function () { | ||
112 | this.timeout(60_000) | ||
113 | |||
114 | const baseAttributes = { | ||
115 | channelId: servers[0].store.channel.id, | ||
116 | privacy: VideoPrivacy.PUBLIC | ||
117 | } | ||
118 | |||
119 | { | ||
120 | const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube } | ||
121 | const { video } = await servers[0].imports.importVideo({ attributes }) | ||
122 | expect(video.name).to.equal('small video - youtube') | ||
123 | |||
124 | { | ||
125 | expect(video.thumbnailPath).to.match(new RegExp(`^/lazy-static/thumbnails/.+.jpg$`)) | ||
126 | expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) | ||
127 | |||
128 | const suffix = mode === 'yt-dlp' | ||
129 | ? '_yt_dlp' | ||
130 | : '' | ||
131 | |||
132 | await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath) | ||
133 | await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_preview' + suffix, video.previewPath) | ||
134 | } | ||
135 | |||
136 | const bodyCaptions = await servers[0].captions.list({ videoId: video.id }) | ||
137 | const videoCaptions = bodyCaptions.data | ||
138 | expect(videoCaptions).to.have.lengthOf(2) | ||
139 | |||
140 | { | ||
141 | const enCaption = videoCaptions.find(caption => caption.language.id === 'en') | ||
142 | expect(enCaption).to.exist | ||
143 | expect(enCaption.language.label).to.equal('English') | ||
144 | expect(enCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-en.vtt$`)) | ||
145 | |||
146 | const regex = `WEBVTT[ \n]+Kind: captions[ \n]+` + | ||
147 | `(Language: en[ \n]+)?` + | ||
148 | `00:00:01.600 --> 00:00:04.200( position:\\d+% line:\\d+%)?[ \n]+English \\(US\\)[ \n]+` + | ||
149 | `00:00:05.900 --> 00:00:07.999( position:\\d+% line:\\d+%)?[ \n]+This is a subtitle in American English[ \n]+` + | ||
150 | `00:00:10.000 --> 00:00:14.000( position:\\d+% line:\\d+%)?[ \n]+Adding subtitles is very easy to do` | ||
151 | await testCaptionFile(servers[0].url, enCaption.captionPath, new RegExp(regex)) | ||
152 | } | ||
153 | |||
154 | { | ||
155 | const frCaption = videoCaptions.find(caption => caption.language.id === 'fr') | ||
156 | expect(frCaption).to.exist | ||
157 | expect(frCaption.language.label).to.equal('French') | ||
158 | expect(frCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-fr.vtt`)) | ||
159 | |||
160 | const regex = `WEBVTT[ \n]+Kind: captions[ \n]+` + | ||
161 | `(Language: fr[ \n]+)?` + | ||
162 | `00:00:01.600 --> 00:00:04.200( position:\\d+% line:\\d+%)?[ \n]+Français \\(FR\\)[ \n]+` + | ||
163 | `00:00:05.900 --> 00:00:07.999( position:\\d+% line:\\d+%)?[ \n]+C'est un sous-titre français[ \n]+` + | ||
164 | `00:00:10.000 --> 00:00:14.000( position:\\d+% line:\\d+%)?[ \n]+Ajouter un sous-titre est vraiment facile` | ||
165 | |||
166 | await testCaptionFile(servers[0].url, frCaption.captionPath, new RegExp(regex)) | ||
167 | } | ||
168 | } | ||
169 | |||
170 | { | ||
171 | const attributes = { | ||
172 | ...baseAttributes, | ||
173 | magnetUri: FIXTURE_URLS.magnet, | ||
174 | description: 'this is a super torrent description', | ||
175 | tags: [ 'tag_torrent1', 'tag_torrent2' ] | ||
176 | } | ||
177 | const { video } = await servers[0].imports.importVideo({ attributes }) | ||
178 | expect(video.name).to.equal('super peertube2 video') | ||
179 | } | ||
180 | |||
181 | { | ||
182 | const attributes = { | ||
183 | ...baseAttributes, | ||
184 | torrentfile: 'video-720p.torrent' as any, | ||
185 | description: 'this is a super torrent description', | ||
186 | tags: [ 'tag_torrent1', 'tag_torrent2' ] | ||
187 | } | ||
188 | const { video } = await servers[0].imports.importVideo({ attributes }) | ||
189 | expect(video.name).to.equal('ä½ å¥½ 世界 720p.mp4') | ||
190 | } | ||
191 | }) | ||
192 | |||
193 | it('Should list the videos to import in my videos on server 1', async function () { | ||
194 | const { total, data } = await servers[0].videos.listMyVideos({ sort: 'createdAt' }) | ||
195 | |||
196 | expect(total).to.equal(3) | ||
197 | |||
198 | expect(data).to.have.lengthOf(3) | ||
199 | expect(data[0].name).to.equal('small video - youtube') | ||
200 | expect(data[1].name).to.equal('super peertube2 video') | ||
201 | expect(data[2].name).to.equal('ä½ å¥½ 世界 720p.mp4') | ||
202 | }) | ||
203 | |||
204 | it('Should list the videos to import in my imports on server 1', async function () { | ||
205 | const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' }) | ||
206 | expect(total).to.equal(3) | ||
207 | |||
208 | expect(videoImports).to.have.lengthOf(3) | ||
209 | |||
210 | expect(videoImports[2].targetUrl).to.equal(FIXTURE_URLS.youtube) | ||
211 | expect(videoImports[2].magnetUri).to.be.null | ||
212 | expect(videoImports[2].torrentName).to.be.null | ||
213 | expect(videoImports[2].video.name).to.equal('small video - youtube') | ||
214 | |||
215 | expect(videoImports[1].targetUrl).to.be.null | ||
216 | expect(videoImports[1].magnetUri).to.equal(FIXTURE_URLS.magnet) | ||
217 | expect(videoImports[1].torrentName).to.be.null | ||
218 | expect(videoImports[1].video.name).to.equal('super peertube2 video') | ||
219 | |||
220 | expect(videoImports[0].targetUrl).to.be.null | ||
221 | expect(videoImports[0].magnetUri).to.be.null | ||
222 | expect(videoImports[0].torrentName).to.equal('video-720p.torrent') | ||
223 | expect(videoImports[0].video.name).to.equal('ä½ å¥½ 世界 720p.mp4') | ||
224 | }) | ||
225 | |||
226 | it('Should filter my imports on target URL', async function () { | ||
227 | const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ targetUrl: FIXTURE_URLS.youtube }) | ||
228 | expect(total).to.equal(1) | ||
229 | expect(videoImports).to.have.lengthOf(1) | ||
230 | |||
231 | expect(videoImports[0].targetUrl).to.equal(FIXTURE_URLS.youtube) | ||
232 | }) | ||
233 | |||
234 | it('Should search in my imports', async function () { | ||
235 | const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ search: 'peertube2' }) | ||
236 | expect(total).to.equal(1) | ||
237 | expect(videoImports).to.have.lengthOf(1) | ||
238 | |||
239 | expect(videoImports[0].magnetUri).to.equal(FIXTURE_URLS.magnet) | ||
240 | expect(videoImports[0].video.name).to.equal('super peertube2 video') | ||
241 | }) | ||
242 | |||
243 | it('Should have the video listed on the two instances', async function () { | ||
244 | this.timeout(120_000) | ||
245 | |||
246 | await waitJobs(servers) | ||
247 | |||
248 | for (const server of servers) { | ||
249 | const { total, data } = await server.videos.list() | ||
250 | expect(total).to.equal(3) | ||
251 | expect(data).to.have.lengthOf(3) | ||
252 | |||
253 | const [ videoHttp, videoMagnet, videoTorrent ] = data | ||
254 | await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid) | ||
255 | } | ||
256 | }) | ||
257 | |||
258 | it('Should import a video on server 2 with some fields', async function () { | ||
259 | this.timeout(60_000) | ||
260 | |||
261 | const { video } = await servers[1].imports.importVideo({ | ||
262 | attributes: { | ||
263 | targetUrl: FIXTURE_URLS.youtube, | ||
264 | channelId: servers[1].store.channel.id, | ||
265 | privacy: VideoPrivacy.PUBLIC, | ||
266 | category: 10, | ||
267 | licence: 7, | ||
268 | language: 'en', | ||
269 | name: 'my super name', | ||
270 | description: 'my super description', | ||
271 | tags: [ 'supertag1', 'supertag2' ], | ||
272 | thumbnailfile: 'custom-thumbnail.jpg' | ||
273 | } | ||
274 | }) | ||
275 | expect(video.name).to.equal('my super name') | ||
276 | }) | ||
277 | |||
278 | it('Should have the videos listed on the two instances', async function () { | ||
279 | this.timeout(120_000) | ||
280 | |||
281 | await waitJobs(servers) | ||
282 | |||
283 | for (const server of servers) { | ||
284 | const { total, data } = await server.videos.list() | ||
285 | expect(total).to.equal(4) | ||
286 | expect(data).to.have.lengthOf(4) | ||
287 | |||
288 | await checkVideoServer2(server, data[0].uuid) | ||
289 | |||
290 | const [ , videoHttp, videoMagnet, videoTorrent ] = data | ||
291 | await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid) | ||
292 | } | ||
293 | }) | ||
294 | |||
295 | it('Should import a video that will be transcoded', async function () { | ||
296 | this.timeout(240_000) | ||
297 | |||
298 | const attributes = { | ||
299 | name: 'transcoded video', | ||
300 | magnetUri: FIXTURE_URLS.magnet, | ||
301 | channelId: servers[1].store.channel.id, | ||
302 | privacy: VideoPrivacy.PUBLIC | ||
303 | } | ||
304 | const { video } = await servers[1].imports.importVideo({ attributes }) | ||
305 | const videoUUID = video.uuid | ||
306 | |||
307 | await waitJobs(servers) | ||
308 | |||
309 | for (const server of servers) { | ||
310 | const video = await server.videos.get({ id: videoUUID }) | ||
311 | |||
312 | expect(video.name).to.equal('transcoded video') | ||
313 | expect(video.files).to.have.lengthOf(4) | ||
314 | } | ||
315 | }) | ||
316 | |||
317 | it('Should import no HDR version on a HDR video', async function () { | ||
318 | this.timeout(300_000) | ||
319 | |||
320 | const config: DeepPartial<CustomConfig> = { | ||
321 | transcoding: { | ||
322 | enabled: true, | ||
323 | resolutions: { | ||
324 | '0p': false, | ||
325 | '144p': true, | ||
326 | '240p': true, | ||
327 | '360p': false, | ||
328 | '480p': false, | ||
329 | '720p': false, | ||
330 | '1080p': false, // the resulting resolution shouldn't be higher than this, and not vp9.2/av01 | ||
331 | '1440p': false, | ||
332 | '2160p': false | ||
333 | }, | ||
334 | webVideos: { enabled: true }, | ||
335 | hls: { enabled: false } | ||
336 | } | ||
337 | } | ||
338 | await servers[0].config.updateExistingSubConfig({ newConfig: config }) | ||
339 | |||
340 | const attributes = { | ||
341 | name: 'hdr video', | ||
342 | targetUrl: FIXTURE_URLS.youtubeHDR, | ||
343 | channelId: servers[0].store.channel.id, | ||
344 | privacy: VideoPrivacy.PUBLIC | ||
345 | } | ||
346 | const { video: videoImported } = await servers[0].imports.importVideo({ attributes }) | ||
347 | const videoUUID = videoImported.uuid | ||
348 | |||
349 | await waitJobs(servers) | ||
350 | |||
351 | // test resolution | ||
352 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
353 | expect(video.name).to.equal('hdr video') | ||
354 | const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id })) | ||
355 | expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_240P) | ||
356 | }) | ||
357 | |||
358 | it('Should not import resolution higher than enabled transcoding resolution', async function () { | ||
359 | this.timeout(300_000) | ||
360 | |||
361 | const config: DeepPartial<CustomConfig> = { | ||
362 | transcoding: { | ||
363 | enabled: true, | ||
364 | resolutions: { | ||
365 | '0p': false, | ||
366 | '144p': true, | ||
367 | '240p': false, | ||
368 | '360p': false, | ||
369 | '480p': false, | ||
370 | '720p': false, | ||
371 | '1080p': false, | ||
372 | '1440p': false, | ||
373 | '2160p': false | ||
374 | }, | ||
375 | alwaysTranscodeOriginalResolution: false | ||
376 | } | ||
377 | } | ||
378 | await servers[0].config.updateExistingSubConfig({ newConfig: config }) | ||
379 | |||
380 | const attributes = { | ||
381 | name: 'small resolution video', | ||
382 | targetUrl: FIXTURE_URLS.youtube, | ||
383 | channelId: servers[0].store.channel.id, | ||
384 | privacy: VideoPrivacy.PUBLIC | ||
385 | } | ||
386 | const { video: videoImported } = await servers[0].imports.importVideo({ attributes }) | ||
387 | const videoUUID = videoImported.uuid | ||
388 | |||
389 | await waitJobs(servers) | ||
390 | |||
391 | // test resolution | ||
392 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
393 | expect(video.name).to.equal('small resolution video') | ||
394 | expect(video.files).to.have.lengthOf(1) | ||
395 | expect(video.files[0].resolution.id).to.equal(144) | ||
396 | }) | ||
397 | |||
398 | it('Should import resolution higher than enabled transcoding resolution', async function () { | ||
399 | this.timeout(300_000) | ||
400 | |||
401 | const config: DeepPartial<CustomConfig> = { | ||
402 | transcoding: { | ||
403 | alwaysTranscodeOriginalResolution: true | ||
404 | } | ||
405 | } | ||
406 | await servers[0].config.updateExistingSubConfig({ newConfig: config }) | ||
407 | |||
408 | const attributes = { | ||
409 | name: 'bigger resolution video', | ||
410 | targetUrl: FIXTURE_URLS.youtube, | ||
411 | channelId: servers[0].store.channel.id, | ||
412 | privacy: VideoPrivacy.PUBLIC | ||
413 | } | ||
414 | const { video: videoImported } = await servers[0].imports.importVideo({ attributes }) | ||
415 | const videoUUID = videoImported.uuid | ||
416 | |||
417 | await waitJobs(servers) | ||
418 | |||
419 | // test resolution | ||
420 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
421 | expect(video.name).to.equal('bigger resolution video') | ||
422 | |||
423 | expect(video.files).to.have.lengthOf(2) | ||
424 | expect(video.files.find(f => f.resolution.id === 240)).to.exist | ||
425 | expect(video.files.find(f => f.resolution.id === 144)).to.exist | ||
426 | }) | ||
427 | |||
428 | it('Should import a peertube video', async function () { | ||
429 | this.timeout(120_000) | ||
430 | |||
431 | const toTest = [ FIXTURE_URLS.peertube_long ] | ||
432 | |||
433 | // TODO: include peertube_short when https://github.com/ytdl-org/youtube-dl/pull/29475 is merged | ||
434 | if (mode === 'yt-dlp') { | ||
435 | toTest.push(FIXTURE_URLS.peertube_short) | ||
436 | } | ||
437 | |||
438 | for (const targetUrl of toTest) { | ||
439 | await servers[0].config.disableTranscoding() | ||
440 | |||
441 | const attributes = { | ||
442 | targetUrl, | ||
443 | channelId: servers[0].store.channel.id, | ||
444 | privacy: VideoPrivacy.PUBLIC | ||
445 | } | ||
446 | const { video } = await servers[0].imports.importVideo({ attributes }) | ||
447 | const videoUUID = video.uuid | ||
448 | |||
449 | await waitJobs(servers) | ||
450 | |||
451 | for (const server of servers) { | ||
452 | const video = await server.videos.get({ id: videoUUID }) | ||
453 | |||
454 | expect(video.name).to.equal('E2E tests') | ||
455 | |||
456 | const { data: captions } = await server.captions.list({ videoId: videoUUID }) | ||
457 | expect(captions).to.have.lengthOf(1) | ||
458 | expect(captions[0].language.id).to.equal('fr') | ||
459 | |||
460 | const str = `WEBVTT FILE\r?\n\r?\n` + | ||
461 | `1\r?\n` + | ||
462 | `00:00:04.000 --> 00:00:09.000\r?\n` + | ||
463 | `January 1, 1994. The North American` | ||
464 | await testCaptionFile(server.url, captions[0].captionPath, new RegExp(str)) | ||
465 | } | ||
466 | } | ||
467 | }) | ||
468 | |||
469 | after(async function () { | ||
470 | await cleanupTests(servers) | ||
471 | }) | ||
472 | }) | ||
473 | } | ||
474 | |||
475 | // FIXME: youtube-dl seems broken | ||
476 | // runSuite('youtube-dl') | ||
477 | |||
478 | runSuite('yt-dlp') | ||
479 | |||
480 | describe('Delete/cancel an import', function () { | ||
481 | let server: PeerTubeServer | ||
482 | |||
483 | let finishedImportId: number | ||
484 | let finishedVideo: Video | ||
485 | let pendingImportId: number | ||
486 | |||
487 | async function importVideo (name: string) { | ||
488 | const attributes = { name, channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo } | ||
489 | const res = await server.imports.importVideo({ attributes }) | ||
490 | |||
491 | return res.id | ||
492 | } | ||
493 | |||
494 | before(async function () { | ||
495 | this.timeout(120_000) | ||
496 | |||
497 | server = await createSingleServer(1) | ||
498 | |||
499 | await setAccessTokensToServers([ server ]) | ||
500 | await setDefaultVideoChannel([ server ]) | ||
501 | |||
502 | finishedImportId = await importVideo('finished') | ||
503 | await waitJobs([ server ]) | ||
504 | |||
505 | await server.jobs.pauseJobQueue() | ||
506 | pendingImportId = await importVideo('pending') | ||
507 | |||
508 | const { data } = await server.imports.getMyVideoImports() | ||
509 | expect(data).to.have.lengthOf(2) | ||
510 | |||
511 | finishedVideo = data.find(i => i.id === finishedImportId).video | ||
512 | }) | ||
513 | |||
514 | it('Should delete a video import', async function () { | ||
515 | await server.imports.delete({ importId: finishedImportId }) | ||
516 | |||
517 | const { data } = await server.imports.getMyVideoImports() | ||
518 | expect(data).to.have.lengthOf(1) | ||
519 | expect(data[0].id).to.equal(pendingImportId) | ||
520 | expect(data[0].state.id).to.equal(VideoImportState.PENDING) | ||
521 | }) | ||
522 | |||
523 | it('Should not have deleted the associated video', async function () { | ||
524 | const video = await server.videos.get({ id: finishedVideo.id, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
525 | expect(video.name).to.equal('finished') | ||
526 | expect(video.state.id).to.equal(VideoState.PUBLISHED) | ||
527 | }) | ||
528 | |||
529 | it('Should cancel a video import', async function () { | ||
530 | await server.imports.cancel({ importId: pendingImportId }) | ||
531 | |||
532 | const { data } = await server.imports.getMyVideoImports() | ||
533 | expect(data).to.have.lengthOf(1) | ||
534 | expect(data[0].id).to.equal(pendingImportId) | ||
535 | expect(data[0].state.id).to.equal(VideoImportState.CANCELLED) | ||
536 | }) | ||
537 | |||
538 | it('Should not have processed the cancelled video import', async function () { | ||
539 | this.timeout(60_000) | ||
540 | |||
541 | await server.jobs.resumeJobQueue() | ||
542 | |||
543 | await waitJobs([ server ]) | ||
544 | |||
545 | const { data } = await server.imports.getMyVideoImports() | ||
546 | expect(data).to.have.lengthOf(1) | ||
547 | expect(data[0].id).to.equal(pendingImportId) | ||
548 | expect(data[0].state.id).to.equal(VideoImportState.CANCELLED) | ||
549 | expect(data[0].video.state.id).to.equal(VideoState.TO_IMPORT) | ||
550 | }) | ||
551 | |||
552 | it('Should delete the cancelled video import', async function () { | ||
553 | await server.imports.delete({ importId: pendingImportId }) | ||
554 | const { data } = await server.imports.getMyVideoImports() | ||
555 | expect(data).to.have.lengthOf(0) | ||
556 | }) | ||
557 | |||
558 | after(async function () { | ||
559 | await cleanupTests([ server ]) | ||
560 | }) | ||
561 | }) | ||
562 | |||
563 | describe('Auto update', function () { | ||
564 | let server: PeerTubeServer | ||
565 | |||
566 | function quickPeerTubeImport () { | ||
567 | const attributes = { | ||
568 | targetUrl: FIXTURE_URLS.peertube_long, | ||
569 | channelId: server.store.channel.id, | ||
570 | privacy: VideoPrivacy.PUBLIC | ||
571 | } | ||
572 | |||
573 | return server.imports.importVideo({ attributes }) | ||
574 | } | ||
575 | |||
576 | async function testBinaryUpdate (releaseUrl: string, releaseName: string) { | ||
577 | await remove(join(server.servers.buildDirectory('bin'), releaseName)) | ||
578 | |||
579 | await server.kill() | ||
580 | await server.run({ | ||
581 | import: { | ||
582 | videos: { | ||
583 | http: { | ||
584 | youtube_dl_release: { | ||
585 | url: releaseUrl, | ||
586 | name: releaseName | ||
587 | } | ||
588 | } | ||
589 | } | ||
590 | } | ||
591 | }) | ||
592 | |||
593 | await quickPeerTubeImport() | ||
594 | |||
595 | const base = server.servers.buildDirectory('bin') | ||
596 | const content = await readdir(base) | ||
597 | const binaryPath = join(base, releaseName) | ||
598 | |||
599 | expect(await pathExists(binaryPath), `${binaryPath} does not exist in ${base} (${content.join(', ')})`).to.be.true | ||
600 | } | ||
601 | |||
602 | before(async function () { | ||
603 | this.timeout(30_000) | ||
604 | |||
605 | // Run servers | ||
606 | server = await createSingleServer(1) | ||
607 | |||
608 | await setAccessTokensToServers([ server ]) | ||
609 | await setDefaultVideoChannel([ server ]) | ||
610 | }) | ||
611 | |||
612 | it('Should update youtube-dl from github URL', async function () { | ||
613 | this.timeout(120_000) | ||
614 | |||
615 | await testBinaryUpdate('https://api.github.com/repos/ytdl-org/youtube-dl/releases', 'youtube-dl') | ||
616 | }) | ||
617 | |||
618 | it('Should update youtube-dl from raw URL', async function () { | ||
619 | this.timeout(120_000) | ||
620 | |||
621 | await testBinaryUpdate('https://yt-dl.org/downloads/latest/youtube-dl', 'youtube-dl') | ||
622 | }) | ||
623 | |||
624 | it('Should update youtube-dl from youtube-dl fork', async function () { | ||
625 | this.timeout(120_000) | ||
626 | |||
627 | await testBinaryUpdate('https://api.github.com/repos/yt-dlp/yt-dlp/releases', 'yt-dlp') | ||
628 | }) | ||
629 | |||
630 | after(async function () { | ||
631 | await cleanupTests([ server ]) | ||
632 | }) | ||
633 | }) | ||
634 | }) | ||
diff --git a/packages/tests/src/api/videos/video-nsfw.ts b/packages/tests/src/api/videos/video-nsfw.ts new file mode 100644 index 000000000..fc5225dd2 --- /dev/null +++ b/packages/tests/src/api/videos/video-nsfw.ts | |||
@@ -0,0 +1,227 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
5 | import { BooleanBothQuery, CustomConfig, ResultList, Video, VideosOverview } from '@peertube/peertube-models' | ||
6 | |||
7 | function createOverviewRes (overview: VideosOverview) { | ||
8 | const videos = overview.categories[0].videos | ||
9 | return { data: videos, total: videos.length } | ||
10 | } | ||
11 | |||
12 | describe('Test video NSFW policy', function () { | ||
13 | let server: PeerTubeServer | ||
14 | let userAccessToken: string | ||
15 | let customConfig: CustomConfig | ||
16 | |||
17 | async function getVideosFunctions (token?: string, query: { nsfw?: BooleanBothQuery } = {}) { | ||
18 | const user = await server.users.getMyInfo() | ||
19 | |||
20 | const channelName = user.videoChannels[0].name | ||
21 | const accountName = user.account.name + '@' + user.account.host | ||
22 | |||
23 | const hasQuery = Object.keys(query).length !== 0 | ||
24 | let promises: Promise<ResultList<Video>>[] | ||
25 | |||
26 | if (token) { | ||
27 | promises = [ | ||
28 | server.search.advancedVideoSearch({ token, search: { search: 'n', sort: '-publishedAt', ...query } }), | ||
29 | server.videos.listWithToken({ token, ...query }), | ||
30 | server.videos.listByAccount({ token, handle: accountName, ...query }), | ||
31 | server.videos.listByChannel({ token, handle: channelName, ...query }) | ||
32 | ] | ||
33 | |||
34 | // Overviews do not support video filters | ||
35 | if (!hasQuery) { | ||
36 | const p = server.overviews.getVideos({ page: 1, token }) | ||
37 | .then(res => createOverviewRes(res)) | ||
38 | promises.push(p) | ||
39 | } | ||
40 | |||
41 | return Promise.all(promises) | ||
42 | } | ||
43 | |||
44 | promises = [ | ||
45 | server.search.searchVideos({ search: 'n', sort: '-publishedAt' }), | ||
46 | server.videos.list(), | ||
47 | server.videos.listByAccount({ token: null, handle: accountName }), | ||
48 | server.videos.listByChannel({ token: null, handle: channelName }) | ||
49 | ] | ||
50 | |||
51 | // Overviews do not support video filters | ||
52 | if (!hasQuery) { | ||
53 | const p = server.overviews.getVideos({ page: 1 }) | ||
54 | .then(res => createOverviewRes(res)) | ||
55 | promises.push(p) | ||
56 | } | ||
57 | |||
58 | return Promise.all(promises) | ||
59 | } | ||
60 | |||
61 | before(async function () { | ||
62 | this.timeout(50000) | ||
63 | server = await createSingleServer(1) | ||
64 | |||
65 | // Get the access tokens | ||
66 | await setAccessTokensToServers([ server ]) | ||
67 | |||
68 | { | ||
69 | const attributes = { name: 'nsfw', nsfw: true, category: 1 } | ||
70 | await server.videos.upload({ attributes }) | ||
71 | } | ||
72 | |||
73 | { | ||
74 | const attributes = { name: 'normal', nsfw: false, category: 1 } | ||
75 | await server.videos.upload({ attributes }) | ||
76 | } | ||
77 | |||
78 | customConfig = await server.config.getCustomConfig() | ||
79 | }) | ||
80 | |||
81 | describe('Instance default NSFW policy', function () { | ||
82 | |||
83 | it('Should display NSFW videos with display default NSFW policy', async function () { | ||
84 | const serverConfig = await server.config.getConfig() | ||
85 | expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display') | ||
86 | |||
87 | for (const body of await getVideosFunctions()) { | ||
88 | expect(body.total).to.equal(2) | ||
89 | |||
90 | const videos = body.data | ||
91 | expect(videos).to.have.lengthOf(2) | ||
92 | expect(videos[0].name).to.equal('normal') | ||
93 | expect(videos[1].name).to.equal('nsfw') | ||
94 | } | ||
95 | }) | ||
96 | |||
97 | it('Should not display NSFW videos with do_not_list default NSFW policy', async function () { | ||
98 | customConfig.instance.defaultNSFWPolicy = 'do_not_list' | ||
99 | await server.config.updateCustomConfig({ newCustomConfig: customConfig }) | ||
100 | |||
101 | const serverConfig = await server.config.getConfig() | ||
102 | expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list') | ||
103 | |||
104 | for (const body of await getVideosFunctions()) { | ||
105 | expect(body.total).to.equal(1) | ||
106 | |||
107 | const videos = body.data | ||
108 | expect(videos).to.have.lengthOf(1) | ||
109 | expect(videos[0].name).to.equal('normal') | ||
110 | } | ||
111 | }) | ||
112 | |||
113 | it('Should display NSFW videos with blur default NSFW policy', async function () { | ||
114 | customConfig.instance.defaultNSFWPolicy = 'blur' | ||
115 | await server.config.updateCustomConfig({ newCustomConfig: customConfig }) | ||
116 | |||
117 | const serverConfig = await server.config.getConfig() | ||
118 | expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur') | ||
119 | |||
120 | for (const body of await getVideosFunctions()) { | ||
121 | expect(body.total).to.equal(2) | ||
122 | |||
123 | const videos = body.data | ||
124 | expect(videos).to.have.lengthOf(2) | ||
125 | expect(videos[0].name).to.equal('normal') | ||
126 | expect(videos[1].name).to.equal('nsfw') | ||
127 | } | ||
128 | }) | ||
129 | }) | ||
130 | |||
131 | describe('User NSFW policy', function () { | ||
132 | |||
133 | it('Should create a user having the default nsfw policy', async function () { | ||
134 | const username = 'user1' | ||
135 | const password = 'my super password' | ||
136 | await server.users.create({ username, password }) | ||
137 | |||
138 | userAccessToken = await server.login.getAccessToken({ username, password }) | ||
139 | |||
140 | const user = await server.users.getMyInfo({ token: userAccessToken }) | ||
141 | expect(user.nsfwPolicy).to.equal('blur') | ||
142 | }) | ||
143 | |||
144 | it('Should display NSFW videos with blur user NSFW policy', async function () { | ||
145 | customConfig.instance.defaultNSFWPolicy = 'do_not_list' | ||
146 | await server.config.updateCustomConfig({ newCustomConfig: customConfig }) | ||
147 | |||
148 | for (const body of await getVideosFunctions(userAccessToken)) { | ||
149 | expect(body.total).to.equal(2) | ||
150 | |||
151 | const videos = body.data | ||
152 | expect(videos).to.have.lengthOf(2) | ||
153 | expect(videos[0].name).to.equal('normal') | ||
154 | expect(videos[1].name).to.equal('nsfw') | ||
155 | } | ||
156 | }) | ||
157 | |||
158 | it('Should display NSFW videos with display user NSFW policy', async function () { | ||
159 | await server.users.updateMe({ nsfwPolicy: 'display' }) | ||
160 | |||
161 | for (const body of await getVideosFunctions(server.accessToken)) { | ||
162 | expect(body.total).to.equal(2) | ||
163 | |||
164 | const videos = body.data | ||
165 | expect(videos).to.have.lengthOf(2) | ||
166 | expect(videos[0].name).to.equal('normal') | ||
167 | expect(videos[1].name).to.equal('nsfw') | ||
168 | } | ||
169 | }) | ||
170 | |||
171 | it('Should not display NSFW videos with do_not_list user NSFW policy', async function () { | ||
172 | await server.users.updateMe({ nsfwPolicy: 'do_not_list' }) | ||
173 | |||
174 | for (const body of await getVideosFunctions(server.accessToken)) { | ||
175 | expect(body.total).to.equal(1) | ||
176 | |||
177 | const videos = body.data | ||
178 | expect(videos).to.have.lengthOf(1) | ||
179 | expect(videos[0].name).to.equal('normal') | ||
180 | } | ||
181 | }) | ||
182 | |||
183 | it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () { | ||
184 | const { total, data } = await server.videos.listMyVideos() | ||
185 | expect(total).to.equal(2) | ||
186 | |||
187 | expect(data).to.have.lengthOf(2) | ||
188 | expect(data[0].name).to.equal('normal') | ||
189 | expect(data[1].name).to.equal('nsfw') | ||
190 | }) | ||
191 | |||
192 | it('Should display NSFW videos when the nsfw param === true', async function () { | ||
193 | for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'true' })) { | ||
194 | expect(body.total).to.equal(1) | ||
195 | |||
196 | const videos = body.data | ||
197 | expect(videos).to.have.lengthOf(1) | ||
198 | expect(videos[0].name).to.equal('nsfw') | ||
199 | } | ||
200 | }) | ||
201 | |||
202 | it('Should hide NSFW videos when the nsfw param === true', async function () { | ||
203 | for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'false' })) { | ||
204 | expect(body.total).to.equal(1) | ||
205 | |||
206 | const videos = body.data | ||
207 | expect(videos).to.have.lengthOf(1) | ||
208 | expect(videos[0].name).to.equal('normal') | ||
209 | } | ||
210 | }) | ||
211 | |||
212 | it('Should display both videos when the nsfw param === both', async function () { | ||
213 | for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'both' })) { | ||
214 | expect(body.total).to.equal(2) | ||
215 | |||
216 | const videos = body.data | ||
217 | expect(videos).to.have.lengthOf(2) | ||
218 | expect(videos[0].name).to.equal('normal') | ||
219 | expect(videos[1].name).to.equal('nsfw') | ||
220 | } | ||
221 | }) | ||
222 | }) | ||
223 | |||
224 | after(async function () { | ||
225 | await cleanupTests([ server ]) | ||
226 | }) | ||
227 | }) | ||
diff --git a/packages/tests/src/api/videos/video-passwords.ts b/packages/tests/src/api/videos/video-passwords.ts new file mode 100644 index 000000000..60e0e28bd --- /dev/null +++ b/packages/tests/src/api/videos/video-passwords.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | VideoPasswordsCommand, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
14 | |||
15 | describe('Test video passwords', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let videoUUID: string | ||
18 | |||
19 | let userAccessTokenServer1: string | ||
20 | |||
21 | let videoPasswords: string[] = [] | ||
22 | let command: VideoPasswordsCommand | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await createSingleServer(1) | ||
28 | |||
29 | await setAccessTokensToServers([ server ]) | ||
30 | |||
31 | for (let i = 0; i < 10; i++) { | ||
32 | videoPasswords.push(`password ${i + 1}`) | ||
33 | } | ||
34 | const { uuid } = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords } }) | ||
35 | videoUUID = uuid | ||
36 | |||
37 | await setDefaultChannelAvatar(server) | ||
38 | await setDefaultAccountAvatar(server) | ||
39 | |||
40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | ||
41 | await setDefaultChannelAvatar(server, 'user1_channel') | ||
42 | await setDefaultAccountAvatar(server, userAccessTokenServer1) | ||
43 | |||
44 | command = server.videoPasswords | ||
45 | }) | ||
46 | |||
47 | it('Should list video passwords', async function () { | ||
48 | const body = await command.list({ videoId: videoUUID }) | ||
49 | |||
50 | expect(body.total).to.equal(10) | ||
51 | expect(body.data).to.be.an('array') | ||
52 | expect(body.data).to.have.lengthOf(10) | ||
53 | }) | ||
54 | |||
55 | it('Should filter passwords on this video', async function () { | ||
56 | const body = await command.list({ videoId: videoUUID, count: 2, start: 3, sort: 'createdAt' }) | ||
57 | |||
58 | expect(body.total).to.equal(10) | ||
59 | expect(body.data).to.be.an('array') | ||
60 | expect(body.data).to.have.lengthOf(2) | ||
61 | expect(body.data[0].password).to.equal('password 4') | ||
62 | expect(body.data[1].password).to.equal('password 5') | ||
63 | }) | ||
64 | |||
65 | it('Should update password for this video', async function () { | ||
66 | videoPasswords = [ 'my super new password 1', 'my super new password 2' ] | ||
67 | |||
68 | await command.updateAll({ videoId: videoUUID, passwords: videoPasswords }) | ||
69 | const body = await command.list({ videoId: videoUUID }) | ||
70 | expect(body.total).to.equal(2) | ||
71 | expect(body.data).to.be.an('array') | ||
72 | expect(body.data).to.have.lengthOf(2) | ||
73 | expect(body.data[0].password).to.equal('my super new password 2') | ||
74 | expect(body.data[1].password).to.equal('my super new password 1') | ||
75 | }) | ||
76 | |||
77 | it('Should delete one password', async function () { | ||
78 | { | ||
79 | const body = await command.list({ videoId: videoUUID }) | ||
80 | expect(body.total).to.equal(2) | ||
81 | expect(body.data).to.be.an('array') | ||
82 | expect(body.data).to.have.lengthOf(2) | ||
83 | await command.remove({ id: body.data[0].id, videoId: videoUUID }) | ||
84 | } | ||
85 | { | ||
86 | const body = await command.list({ videoId: videoUUID }) | ||
87 | |||
88 | expect(body.total).to.equal(1) | ||
89 | expect(body.data).to.be.an('array') | ||
90 | expect(body.data).to.have.lengthOf(1) | ||
91 | } | ||
92 | }) | ||
93 | |||
94 | after(async function () { | ||
95 | await cleanupTests([ server ]) | ||
96 | }) | ||
97 | }) | ||
diff --git a/packages/tests/src/api/videos/video-playlist-thumbnails.ts b/packages/tests/src/api/videos/video-playlist-thumbnails.ts new file mode 100644 index 000000000..d79c92f72 --- /dev/null +++ b/packages/tests/src/api/videos/video-playlist-thumbnails.ts | |||
@@ -0,0 +1,234 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
5 | import { VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Playlist thumbnail', function () { | ||
17 | let servers: PeerTubeServer[] = [] | ||
18 | |||
19 | let playlistWithoutThumbnailId: number | ||
20 | let playlistWithThumbnailId: number | ||
21 | |||
22 | let withThumbnailE1: number | ||
23 | let withThumbnailE2: number | ||
24 | let withoutThumbnailE1: number | ||
25 | let withoutThumbnailE2: number | ||
26 | |||
27 | let video1: number | ||
28 | let video2: number | ||
29 | |||
30 | async function getPlaylistWithoutThumbnail (server: PeerTubeServer) { | ||
31 | const body = await server.playlists.list({ start: 0, count: 10 }) | ||
32 | |||
33 | return body.data.find(p => p.displayName === 'playlist without thumbnail') | ||
34 | } | ||
35 | |||
36 | async function getPlaylistWithThumbnail (server: PeerTubeServer) { | ||
37 | const body = await server.playlists.list({ start: 0, count: 10 }) | ||
38 | |||
39 | return body.data.find(p => p.displayName === 'playlist with thumbnail') | ||
40 | } | ||
41 | |||
42 | before(async function () { | ||
43 | this.timeout(120000) | ||
44 | |||
45 | servers = await createMultipleServers(2) | ||
46 | |||
47 | // Get the access tokens | ||
48 | await setAccessTokensToServers(servers) | ||
49 | await setDefaultVideoChannel(servers) | ||
50 | |||
51 | for (const server of servers) { | ||
52 | await server.config.disableTranscoding() | ||
53 | } | ||
54 | |||
55 | // Server 1 and server 2 follow each other | ||
56 | await doubleFollow(servers[0], servers[1]) | ||
57 | |||
58 | video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).id | ||
59 | video2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).id | ||
60 | |||
61 | await waitJobs(servers) | ||
62 | }) | ||
63 | |||
64 | it('Should automatically update the thumbnail when adding an element', async function () { | ||
65 | this.timeout(30000) | ||
66 | |||
67 | const created = await servers[1].playlists.create({ | ||
68 | attributes: { | ||
69 | displayName: 'playlist without thumbnail', | ||
70 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
71 | videoChannelId: servers[1].store.channel.id | ||
72 | } | ||
73 | }) | ||
74 | playlistWithoutThumbnailId = created.id | ||
75 | |||
76 | const added = await servers[1].playlists.addElement({ | ||
77 | playlistId: playlistWithoutThumbnailId, | ||
78 | attributes: { videoId: video1 } | ||
79 | }) | ||
80 | withoutThumbnailE1 = added.id | ||
81 | |||
82 | await waitJobs(servers) | ||
83 | |||
84 | for (const server of servers) { | ||
85 | const p = await getPlaylistWithoutThumbnail(server) | ||
86 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) | ||
87 | } | ||
88 | }) | ||
89 | |||
90 | it('Should not update the thumbnail if we explicitly uploaded a thumbnail', async function () { | ||
91 | this.timeout(30000) | ||
92 | |||
93 | const created = await servers[1].playlists.create({ | ||
94 | attributes: { | ||
95 | displayName: 'playlist with thumbnail', | ||
96 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
97 | videoChannelId: servers[1].store.channel.id, | ||
98 | thumbnailfile: 'custom-thumbnail.jpg' | ||
99 | } | ||
100 | }) | ||
101 | playlistWithThumbnailId = created.id | ||
102 | |||
103 | const added = await servers[1].playlists.addElement({ | ||
104 | playlistId: playlistWithThumbnailId, | ||
105 | attributes: { videoId: video1 } | ||
106 | }) | ||
107 | withThumbnailE1 = added.id | ||
108 | |||
109 | await waitJobs(servers) | ||
110 | |||
111 | for (const server of servers) { | ||
112 | const p = await getPlaylistWithThumbnail(server) | ||
113 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) | ||
114 | } | ||
115 | }) | ||
116 | |||
117 | it('Should automatically update the thumbnail when moving the first element', async function () { | ||
118 | this.timeout(30000) | ||
119 | |||
120 | const added = await servers[1].playlists.addElement({ | ||
121 | playlistId: playlistWithoutThumbnailId, | ||
122 | attributes: { videoId: video2 } | ||
123 | }) | ||
124 | withoutThumbnailE2 = added.id | ||
125 | |||
126 | await servers[1].playlists.reorderElements({ | ||
127 | playlistId: playlistWithoutThumbnailId, | ||
128 | attributes: { | ||
129 | startPosition: 1, | ||
130 | insertAfterPosition: 2 | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | await waitJobs(servers) | ||
135 | |||
136 | for (const server of servers) { | ||
137 | const p = await getPlaylistWithoutThumbnail(server) | ||
138 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) | ||
139 | } | ||
140 | }) | ||
141 | |||
142 | it('Should not update the thumbnail when moving the first element if we explicitly uploaded a thumbnail', async function () { | ||
143 | this.timeout(30000) | ||
144 | |||
145 | const added = await servers[1].playlists.addElement({ | ||
146 | playlistId: playlistWithThumbnailId, | ||
147 | attributes: { videoId: video2 } | ||
148 | }) | ||
149 | withThumbnailE2 = added.id | ||
150 | |||
151 | await servers[1].playlists.reorderElements({ | ||
152 | playlistId: playlistWithThumbnailId, | ||
153 | attributes: { | ||
154 | startPosition: 1, | ||
155 | insertAfterPosition: 2 | ||
156 | } | ||
157 | }) | ||
158 | |||
159 | await waitJobs(servers) | ||
160 | |||
161 | for (const server of servers) { | ||
162 | const p = await getPlaylistWithThumbnail(server) | ||
163 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) | ||
164 | } | ||
165 | }) | ||
166 | |||
167 | it('Should automatically update the thumbnail when deleting the first element', async function () { | ||
168 | this.timeout(30000) | ||
169 | |||
170 | await servers[1].playlists.removeElement({ | ||
171 | playlistId: playlistWithoutThumbnailId, | ||
172 | elementId: withoutThumbnailE1 | ||
173 | }) | ||
174 | |||
175 | await waitJobs(servers) | ||
176 | |||
177 | for (const server of servers) { | ||
178 | const p = await getPlaylistWithoutThumbnail(server) | ||
179 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath) | ||
180 | } | ||
181 | }) | ||
182 | |||
183 | it('Should not update the thumbnail when deleting the first element if we explicitly uploaded a thumbnail', async function () { | ||
184 | this.timeout(30000) | ||
185 | |||
186 | await servers[1].playlists.removeElement({ | ||
187 | playlistId: playlistWithThumbnailId, | ||
188 | elementId: withThumbnailE1 | ||
189 | }) | ||
190 | |||
191 | await waitJobs(servers) | ||
192 | |||
193 | for (const server of servers) { | ||
194 | const p = await getPlaylistWithThumbnail(server) | ||
195 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) | ||
196 | } | ||
197 | }) | ||
198 | |||
199 | it('Should the thumbnail when we delete the last element', async function () { | ||
200 | this.timeout(30000) | ||
201 | |||
202 | await servers[1].playlists.removeElement({ | ||
203 | playlistId: playlistWithoutThumbnailId, | ||
204 | elementId: withoutThumbnailE2 | ||
205 | }) | ||
206 | |||
207 | await waitJobs(servers) | ||
208 | |||
209 | for (const server of servers) { | ||
210 | const p = await getPlaylistWithoutThumbnail(server) | ||
211 | expect(p.thumbnailPath).to.be.null | ||
212 | } | ||
213 | }) | ||
214 | |||
215 | it('Should not update the thumbnail when we delete the last element if we explicitly uploaded a thumbnail', async function () { | ||
216 | this.timeout(30000) | ||
217 | |||
218 | await servers[1].playlists.removeElement({ | ||
219 | playlistId: playlistWithThumbnailId, | ||
220 | elementId: withThumbnailE2 | ||
221 | }) | ||
222 | |||
223 | await waitJobs(servers) | ||
224 | |||
225 | for (const server of servers) { | ||
226 | const p = await getPlaylistWithThumbnail(server) | ||
227 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath) | ||
228 | } | ||
229 | }) | ||
230 | |||
231 | after(async function () { | ||
232 | await cleanupTests(servers) | ||
233 | }) | ||
234 | }) | ||
diff --git a/packages/tests/src/api/videos/video-playlists.ts b/packages/tests/src/api/videos/video-playlists.ts new file mode 100644 index 000000000..578d01093 --- /dev/null +++ b/packages/tests/src/api/videos/video-playlists.ts | |||
@@ -0,0 +1,1210 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | HttpStatusCode, | ||
7 | VideoPlaylist, | ||
8 | VideoPlaylistCreateResult, | ||
9 | VideoPlaylistElementType, | ||
10 | VideoPlaylistElementType_Type, | ||
11 | VideoPlaylistPrivacy, | ||
12 | VideoPlaylistType, | ||
13 | VideoPrivacy | ||
14 | } from '@peertube/peertube-models' | ||
15 | import { uuidToShort } from '@peertube/peertube-node-utils' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | PeerTubeServer, | ||
21 | PlaylistsCommand, | ||
22 | setAccessTokensToServers, | ||
23 | setDefaultAccountAvatar, | ||
24 | setDefaultVideoChannel, | ||
25 | waitJobs | ||
26 | } from '@peertube/peertube-server-commands' | ||
27 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
28 | import { checkPlaylistFilesWereRemoved } from '@tests/shared/video-playlists.js' | ||
29 | |||
30 | async function checkPlaylistElementType ( | ||
31 | servers: PeerTubeServer[], | ||
32 | playlistId: string, | ||
33 | type: VideoPlaylistElementType_Type, | ||
34 | position: number, | ||
35 | name: string, | ||
36 | total: number | ||
37 | ) { | ||
38 | for (const server of servers) { | ||
39 | const body = await server.playlists.listVideos({ token: server.accessToken, playlistId, start: 0, count: 10 }) | ||
40 | expect(body.total).to.equal(total) | ||
41 | |||
42 | const videoElement = body.data.find(e => e.position === position) | ||
43 | expect(videoElement.type).to.equal(type, 'On server ' + server.url) | ||
44 | |||
45 | if (type === VideoPlaylistElementType.REGULAR) { | ||
46 | expect(videoElement.video).to.not.be.null | ||
47 | expect(videoElement.video.name).to.equal(name) | ||
48 | } else { | ||
49 | expect(videoElement.video).to.be.null | ||
50 | } | ||
51 | } | ||
52 | } | ||
53 | |||
54 | describe('Test video playlists', function () { | ||
55 | let servers: PeerTubeServer[] = [] | ||
56 | |||
57 | let playlistServer2Id1: number | ||
58 | let playlistServer2Id2: number | ||
59 | let playlistServer2UUID2: string | ||
60 | |||
61 | let playlistServer1Id: number | ||
62 | let playlistServer1DisplayName: string | ||
63 | let playlistServer1UUID: string | ||
64 | let playlistServer1UUID2: string | ||
65 | |||
66 | let playlistElementServer1Video4: number | ||
67 | let playlistElementServer1Video5: number | ||
68 | let playlistElementNSFW: number | ||
69 | |||
70 | let nsfwVideoServer1: number | ||
71 | |||
72 | let userTokenServer1: string | ||
73 | |||
74 | let commands: PlaylistsCommand[] | ||
75 | |||
76 | before(async function () { | ||
77 | this.timeout(240000) | ||
78 | |||
79 | servers = await createMultipleServers(3) | ||
80 | |||
81 | // Get the access tokens | ||
82 | await setAccessTokensToServers(servers) | ||
83 | await setDefaultVideoChannel(servers) | ||
84 | await setDefaultAccountAvatar(servers) | ||
85 | |||
86 | for (const server of servers) { | ||
87 | await server.config.disableTranscoding() | ||
88 | } | ||
89 | |||
90 | // Server 1 and server 2 follow each other | ||
91 | await doubleFollow(servers[0], servers[1]) | ||
92 | // Server 1 and server 3 follow each other | ||
93 | await doubleFollow(servers[0], servers[2]) | ||
94 | |||
95 | commands = servers.map(s => s.playlists) | ||
96 | |||
97 | { | ||
98 | servers[0].store.videos = [] | ||
99 | servers[1].store.videos = [] | ||
100 | servers[2].store.videos = [] | ||
101 | |||
102 | for (const server of servers) { | ||
103 | for (let i = 0; i < 7; i++) { | ||
104 | const name = `video ${i} server ${server.serverNumber}` | ||
105 | const video = await server.videos.upload({ attributes: { name, nsfw: false } }) | ||
106 | |||
107 | server.store.videos.push(video) | ||
108 | } | ||
109 | } | ||
110 | } | ||
111 | |||
112 | nsfwVideoServer1 = (await servers[0].videos.quickUpload({ name: 'NSFW video', nsfw: true })).id | ||
113 | |||
114 | userTokenServer1 = await servers[0].users.generateUserAndToken('user1') | ||
115 | |||
116 | await waitJobs(servers) | ||
117 | }) | ||
118 | |||
119 | describe('Check playlists filters and privacies', function () { | ||
120 | |||
121 | it('Should list video playlist privacies', async function () { | ||
122 | const privacies = await commands[0].getPrivacies() | ||
123 | |||
124 | expect(Object.keys(privacies)).to.have.length.at.least(3) | ||
125 | expect(privacies[3]).to.equal('Private') | ||
126 | }) | ||
127 | |||
128 | it('Should filter on playlist type', async function () { | ||
129 | this.timeout(30000) | ||
130 | |||
131 | const token = servers[0].accessToken | ||
132 | |||
133 | await commands[0].create({ | ||
134 | attributes: { | ||
135 | displayName: 'my super playlist', | ||
136 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
137 | description: 'my super description', | ||
138 | thumbnailfile: 'custom-thumbnail.jpg', | ||
139 | videoChannelId: servers[0].store.channel.id | ||
140 | } | ||
141 | }) | ||
142 | |||
143 | { | ||
144 | const body = await commands[0].listByAccount({ token, handle: 'root', playlistType: VideoPlaylistType.WATCH_LATER }) | ||
145 | |||
146 | expect(body.total).to.equal(1) | ||
147 | expect(body.data).to.have.lengthOf(1) | ||
148 | |||
149 | const playlist = body.data[0] | ||
150 | expect(playlist.displayName).to.equal('Watch later') | ||
151 | expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER) | ||
152 | expect(playlist.type.label).to.equal('Watch later') | ||
153 | expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) | ||
154 | } | ||
155 | |||
156 | { | ||
157 | const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.WATCH_LATER }) | ||
158 | const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.WATCH_LATER }) | ||
159 | |||
160 | for (const body of [ bodyList, bodyChannel ]) { | ||
161 | expect(body.total).to.equal(0) | ||
162 | expect(body.data).to.have.lengthOf(0) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | { | ||
167 | const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.REGULAR }) | ||
168 | const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.REGULAR }) | ||
169 | |||
170 | let playlist: VideoPlaylist = null | ||
171 | for (const body of [ bodyList, bodyChannel ]) { | ||
172 | |||
173 | expect(body.total).to.equal(1) | ||
174 | expect(body.data).to.have.lengthOf(1) | ||
175 | |||
176 | playlist = body.data[0] | ||
177 | expect(playlist.displayName).to.equal('my super playlist') | ||
178 | expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) | ||
179 | expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) | ||
180 | } | ||
181 | |||
182 | await commands[0].update({ | ||
183 | playlistId: playlist.id, | ||
184 | attributes: { | ||
185 | privacy: VideoPlaylistPrivacy.PRIVATE | ||
186 | } | ||
187 | }) | ||
188 | } | ||
189 | |||
190 | { | ||
191 | const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.REGULAR }) | ||
192 | const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.REGULAR }) | ||
193 | |||
194 | for (const body of [ bodyList, bodyChannel ]) { | ||
195 | expect(body.total).to.equal(0) | ||
196 | expect(body.data).to.have.lengthOf(0) | ||
197 | } | ||
198 | } | ||
199 | |||
200 | { | ||
201 | const body = await commands[0].listByAccount({ handle: 'root' }) | ||
202 | expect(body.total).to.equal(0) | ||
203 | expect(body.data).to.have.lengthOf(0) | ||
204 | } | ||
205 | }) | ||
206 | |||
207 | it('Should get private playlist for a classic user', async function () { | ||
208 | const token = await servers[0].users.generateUserAndToken('toto') | ||
209 | |||
210 | const body = await commands[0].listByAccount({ token, handle: 'toto' }) | ||
211 | |||
212 | expect(body.total).to.equal(1) | ||
213 | expect(body.data).to.have.lengthOf(1) | ||
214 | |||
215 | const playlistId = body.data[0].id | ||
216 | await commands[0].listVideos({ token, playlistId }) | ||
217 | }) | ||
218 | }) | ||
219 | |||
220 | describe('Create and federate playlists', function () { | ||
221 | |||
222 | it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () { | ||
223 | this.timeout(30000) | ||
224 | |||
225 | await commands[0].create({ | ||
226 | attributes: { | ||
227 | displayName: 'my super playlist', | ||
228 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
229 | description: 'my super description', | ||
230 | thumbnailfile: 'custom-thumbnail.jpg', | ||
231 | videoChannelId: servers[0].store.channel.id | ||
232 | } | ||
233 | }) | ||
234 | |||
235 | await waitJobs(servers) | ||
236 | // Processing a playlist by the receiver could be long | ||
237 | await wait(3000) | ||
238 | |||
239 | for (const server of servers) { | ||
240 | const body = await server.playlists.list({ start: 0, count: 5 }) | ||
241 | expect(body.total).to.equal(1) | ||
242 | expect(body.data).to.have.lengthOf(1) | ||
243 | |||
244 | const playlistFromList = body.data[0] | ||
245 | |||
246 | const playlistFromGet = await server.playlists.get({ playlistId: playlistFromList.uuid }) | ||
247 | |||
248 | for (const playlist of [ playlistFromGet, playlistFromList ]) { | ||
249 | expect(playlist.id).to.be.a('number') | ||
250 | expect(playlist.uuid).to.be.a('string') | ||
251 | |||
252 | expect(playlist.isLocal).to.equal(server.serverNumber === 1) | ||
253 | |||
254 | expect(playlist.displayName).to.equal('my super playlist') | ||
255 | expect(playlist.description).to.equal('my super description') | ||
256 | expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) | ||
257 | expect(playlist.privacy.label).to.equal('Public') | ||
258 | expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) | ||
259 | expect(playlist.type.label).to.equal('Regular') | ||
260 | expect(playlist.embedPath).to.equal('/video-playlists/embed/' + playlist.uuid) | ||
261 | |||
262 | expect(playlist.videosLength).to.equal(0) | ||
263 | |||
264 | expect(playlist.ownerAccount.name).to.equal('root') | ||
265 | expect(playlist.ownerAccount.displayName).to.equal('root') | ||
266 | expect(playlist.videoChannel.name).to.equal('root_channel') | ||
267 | expect(playlist.videoChannel.displayName).to.equal('Main root channel') | ||
268 | } | ||
269 | } | ||
270 | }) | ||
271 | |||
272 | it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () { | ||
273 | this.timeout(30000) | ||
274 | |||
275 | { | ||
276 | const playlist = await servers[1].playlists.create({ | ||
277 | attributes: { | ||
278 | displayName: 'playlist 2', | ||
279 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
280 | videoChannelId: servers[1].store.channel.id | ||
281 | } | ||
282 | }) | ||
283 | playlistServer2Id1 = playlist.id | ||
284 | } | ||
285 | |||
286 | { | ||
287 | const playlist = await servers[1].playlists.create({ | ||
288 | attributes: { | ||
289 | displayName: 'playlist 3', | ||
290 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
291 | thumbnailfile: 'custom-thumbnail.jpg', | ||
292 | videoChannelId: servers[1].store.channel.id | ||
293 | } | ||
294 | }) | ||
295 | |||
296 | playlistServer2Id2 = playlist.id | ||
297 | playlistServer2UUID2 = playlist.uuid | ||
298 | } | ||
299 | |||
300 | for (const id of [ playlistServer2Id1, playlistServer2Id2 ]) { | ||
301 | await servers[1].playlists.addElement({ | ||
302 | playlistId: id, | ||
303 | attributes: { videoId: servers[1].store.videos[0].id, startTimestamp: 1, stopTimestamp: 2 } | ||
304 | }) | ||
305 | await servers[1].playlists.addElement({ | ||
306 | playlistId: id, | ||
307 | attributes: { videoId: servers[1].store.videos[1].id } | ||
308 | }) | ||
309 | } | ||
310 | |||
311 | await waitJobs(servers) | ||
312 | await wait(3000) | ||
313 | |||
314 | for (const server of [ servers[0], servers[1] ]) { | ||
315 | const body = await server.playlists.list({ start: 0, count: 5 }) | ||
316 | |||
317 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') | ||
318 | expect(playlist2).to.not.be.undefined | ||
319 | await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) | ||
320 | |||
321 | const playlist3 = body.data.find(p => p.displayName === 'playlist 3') | ||
322 | expect(playlist3).to.not.be.undefined | ||
323 | await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', playlist3.thumbnailPath) | ||
324 | } | ||
325 | |||
326 | const body = await servers[2].playlists.list({ start: 0, count: 5 }) | ||
327 | expect(body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined | ||
328 | expect(body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined | ||
329 | }) | ||
330 | |||
331 | it('Should have the playlist on server 3 after a new follow', async function () { | ||
332 | this.timeout(30000) | ||
333 | |||
334 | // Server 2 and server 3 follow each other | ||
335 | await doubleFollow(servers[1], servers[2]) | ||
336 | |||
337 | const body = await servers[2].playlists.list({ start: 0, count: 5 }) | ||
338 | |||
339 | const playlist2 = body.data.find(p => p.displayName === 'playlist 2') | ||
340 | expect(playlist2).to.not.be.undefined | ||
341 | await testImageGeneratedByFFmpeg(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath) | ||
342 | |||
343 | expect(body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined | ||
344 | }) | ||
345 | }) | ||
346 | |||
347 | describe('List playlists', function () { | ||
348 | |||
349 | it('Should correctly list the playlists', async function () { | ||
350 | this.timeout(30000) | ||
351 | |||
352 | { | ||
353 | const body = await servers[2].playlists.list({ start: 1, count: 2, sort: 'createdAt' }) | ||
354 | expect(body.total).to.equal(3) | ||
355 | |||
356 | const data = body.data | ||
357 | expect(data).to.have.lengthOf(2) | ||
358 | expect(data[0].displayName).to.equal('playlist 2') | ||
359 | expect(data[1].displayName).to.equal('playlist 3') | ||
360 | } | ||
361 | |||
362 | { | ||
363 | const body = await servers[2].playlists.list({ start: 1, count: 2, sort: '-createdAt' }) | ||
364 | expect(body.total).to.equal(3) | ||
365 | |||
366 | const data = body.data | ||
367 | expect(data).to.have.lengthOf(2) | ||
368 | expect(data[0].displayName).to.equal('playlist 2') | ||
369 | expect(data[1].displayName).to.equal('my super playlist') | ||
370 | } | ||
371 | }) | ||
372 | |||
373 | it('Should list video channel playlists', async function () { | ||
374 | this.timeout(30000) | ||
375 | |||
376 | { | ||
377 | const body = await commands[0].listByChannel({ handle: 'root_channel', start: 0, count: 2, sort: '-createdAt' }) | ||
378 | expect(body.total).to.equal(1) | ||
379 | |||
380 | const data = body.data | ||
381 | expect(data).to.have.lengthOf(1) | ||
382 | expect(data[0].displayName).to.equal('my super playlist') | ||
383 | } | ||
384 | }) | ||
385 | |||
386 | it('Should list account playlists', async function () { | ||
387 | this.timeout(30000) | ||
388 | |||
389 | { | ||
390 | const body = await servers[1].playlists.listByAccount({ handle: 'root', start: 1, count: 2, sort: '-createdAt' }) | ||
391 | expect(body.total).to.equal(2) | ||
392 | |||
393 | const data = body.data | ||
394 | expect(data).to.have.lengthOf(1) | ||
395 | expect(data[0].displayName).to.equal('playlist 2') | ||
396 | } | ||
397 | |||
398 | { | ||
399 | const body = await servers[1].playlists.listByAccount({ handle: 'root', start: 1, count: 2, sort: 'createdAt' }) | ||
400 | expect(body.total).to.equal(2) | ||
401 | |||
402 | const data = body.data | ||
403 | expect(data).to.have.lengthOf(1) | ||
404 | expect(data[0].displayName).to.equal('playlist 3') | ||
405 | } | ||
406 | |||
407 | { | ||
408 | const body = await servers[1].playlists.listByAccount({ handle: 'root', sort: 'createdAt', search: '3' }) | ||
409 | expect(body.total).to.equal(1) | ||
410 | |||
411 | const data = body.data | ||
412 | expect(data).to.have.lengthOf(1) | ||
413 | expect(data[0].displayName).to.equal('playlist 3') | ||
414 | } | ||
415 | |||
416 | { | ||
417 | const body = await servers[1].playlists.listByAccount({ handle: 'root', sort: 'createdAt', search: '4' }) | ||
418 | expect(body.total).to.equal(0) | ||
419 | |||
420 | const data = body.data | ||
421 | expect(data).to.have.lengthOf(0) | ||
422 | } | ||
423 | }) | ||
424 | }) | ||
425 | |||
426 | describe('Playlist rights', function () { | ||
427 | let unlistedPlaylist: VideoPlaylistCreateResult | ||
428 | let privatePlaylist: VideoPlaylistCreateResult | ||
429 | |||
430 | before(async function () { | ||
431 | this.timeout(30000) | ||
432 | |||
433 | { | ||
434 | unlistedPlaylist = await servers[1].playlists.create({ | ||
435 | attributes: { | ||
436 | displayName: 'playlist unlisted', | ||
437 | privacy: VideoPlaylistPrivacy.UNLISTED, | ||
438 | videoChannelId: servers[1].store.channel.id | ||
439 | } | ||
440 | }) | ||
441 | } | ||
442 | |||
443 | { | ||
444 | privatePlaylist = await servers[1].playlists.create({ | ||
445 | attributes: { | ||
446 | displayName: 'playlist private', | ||
447 | privacy: VideoPlaylistPrivacy.PRIVATE | ||
448 | } | ||
449 | }) | ||
450 | } | ||
451 | |||
452 | await waitJobs(servers) | ||
453 | await wait(3000) | ||
454 | }) | ||
455 | |||
456 | it('Should not list unlisted or private playlists', async function () { | ||
457 | for (const server of servers) { | ||
458 | const results = [ | ||
459 | await server.playlists.listByAccount({ handle: 'root@' + servers[1].host, sort: '-createdAt' }), | ||
460 | await server.playlists.list({ start: 0, count: 2, sort: '-createdAt' }) | ||
461 | ] | ||
462 | |||
463 | expect(results[0].total).to.equal(2) | ||
464 | expect(results[1].total).to.equal(3) | ||
465 | |||
466 | for (const body of results) { | ||
467 | const data = body.data | ||
468 | expect(data).to.have.lengthOf(2) | ||
469 | expect(data[0].displayName).to.equal('playlist 3') | ||
470 | expect(data[1].displayName).to.equal('playlist 2') | ||
471 | } | ||
472 | } | ||
473 | }) | ||
474 | |||
475 | it('Should not get unlisted playlist using only the id', async function () { | ||
476 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 }) | ||
477 | }) | ||
478 | |||
479 | it('Should get unlisted playlist using uuid or shortUUID', async function () { | ||
480 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid }) | ||
481 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID }) | ||
482 | }) | ||
483 | |||
484 | it('Should not get private playlist without token', async function () { | ||
485 | for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) { | ||
486 | await servers[1].playlists.get({ playlistId: id, expectedStatus: 401 }) | ||
487 | } | ||
488 | }) | ||
489 | |||
490 | it('Should get private playlist with a token', async function () { | ||
491 | for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) { | ||
492 | await servers[1].playlists.get({ token: servers[1].accessToken, playlistId: id }) | ||
493 | } | ||
494 | }) | ||
495 | }) | ||
496 | |||
497 | describe('Update playlists', function () { | ||
498 | |||
499 | it('Should update a playlist', async function () { | ||
500 | this.timeout(30000) | ||
501 | |||
502 | await servers[1].playlists.update({ | ||
503 | attributes: { | ||
504 | displayName: 'playlist 3 updated', | ||
505 | description: 'description updated', | ||
506 | privacy: VideoPlaylistPrivacy.UNLISTED, | ||
507 | thumbnailfile: 'custom-thumbnail.jpg', | ||
508 | videoChannelId: servers[1].store.channel.id | ||
509 | }, | ||
510 | playlistId: playlistServer2Id2 | ||
511 | }) | ||
512 | |||
513 | await waitJobs(servers) | ||
514 | |||
515 | for (const server of servers) { | ||
516 | const playlist = await server.playlists.get({ playlistId: playlistServer2UUID2 }) | ||
517 | |||
518 | expect(playlist.displayName).to.equal('playlist 3 updated') | ||
519 | expect(playlist.description).to.equal('description updated') | ||
520 | |||
521 | expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED) | ||
522 | expect(playlist.privacy.label).to.equal('Unlisted') | ||
523 | |||
524 | expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) | ||
525 | expect(playlist.type.label).to.equal('Regular') | ||
526 | |||
527 | expect(playlist.videosLength).to.equal(2) | ||
528 | |||
529 | expect(playlist.ownerAccount.name).to.equal('root') | ||
530 | expect(playlist.ownerAccount.displayName).to.equal('root') | ||
531 | expect(playlist.videoChannel.name).to.equal('root_channel') | ||
532 | expect(playlist.videoChannel.displayName).to.equal('Main root channel') | ||
533 | } | ||
534 | }) | ||
535 | }) | ||
536 | |||
537 | describe('Element timestamps', function () { | ||
538 | |||
539 | it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () { | ||
540 | this.timeout(30000) | ||
541 | |||
542 | const addVideo = (attributes: any) => { | ||
543 | return commands[0].addElement({ playlistId: playlistServer1Id, attributes }) | ||
544 | } | ||
545 | |||
546 | const playlistDisplayName = 'playlist 4' | ||
547 | const playlist = await commands[0].create({ | ||
548 | attributes: { | ||
549 | displayName: playlistDisplayName, | ||
550 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
551 | videoChannelId: servers[0].store.channel.id | ||
552 | } | ||
553 | }) | ||
554 | |||
555 | playlistServer1Id = playlist.id | ||
556 | playlistServer1DisplayName = playlistDisplayName | ||
557 | playlistServer1UUID = playlist.uuid | ||
558 | |||
559 | await addVideo({ videoId: servers[0].store.videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 }) | ||
560 | await addVideo({ videoId: servers[2].store.videos[1].uuid, startTimestamp: 35 }) | ||
561 | await addVideo({ videoId: servers[2].store.videos[2].uuid }) | ||
562 | { | ||
563 | const element = await addVideo({ videoId: servers[0].store.videos[3].uuid, stopTimestamp: 35 }) | ||
564 | playlistElementServer1Video4 = element.id | ||
565 | } | ||
566 | |||
567 | { | ||
568 | const element = await addVideo({ videoId: servers[0].store.videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 }) | ||
569 | playlistElementServer1Video5 = element.id | ||
570 | } | ||
571 | |||
572 | { | ||
573 | const element = await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 }) | ||
574 | playlistElementNSFW = element.id | ||
575 | |||
576 | await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 4 }) | ||
577 | await addVideo({ videoId: nsfwVideoServer1 }) | ||
578 | } | ||
579 | |||
580 | await waitJobs(servers) | ||
581 | }) | ||
582 | |||
583 | it('Should correctly list playlist videos', async function () { | ||
584 | this.timeout(30000) | ||
585 | |||
586 | for (const server of servers) { | ||
587 | { | ||
588 | const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 }) | ||
589 | |||
590 | expect(body.total).to.equal(8) | ||
591 | |||
592 | const videoElements = body.data | ||
593 | expect(videoElements).to.have.lengthOf(8) | ||
594 | |||
595 | expect(videoElements[0].video.name).to.equal('video 0 server 1') | ||
596 | expect(videoElements[0].position).to.equal(1) | ||
597 | expect(videoElements[0].startTimestamp).to.equal(15) | ||
598 | expect(videoElements[0].stopTimestamp).to.equal(28) | ||
599 | |||
600 | expect(videoElements[1].video.name).to.equal('video 1 server 3') | ||
601 | expect(videoElements[1].position).to.equal(2) | ||
602 | expect(videoElements[1].startTimestamp).to.equal(35) | ||
603 | expect(videoElements[1].stopTimestamp).to.be.null | ||
604 | |||
605 | expect(videoElements[2].video.name).to.equal('video 2 server 3') | ||
606 | expect(videoElements[2].position).to.equal(3) | ||
607 | expect(videoElements[2].startTimestamp).to.be.null | ||
608 | expect(videoElements[2].stopTimestamp).to.be.null | ||
609 | |||
610 | expect(videoElements[3].video.name).to.equal('video 3 server 1') | ||
611 | expect(videoElements[3].position).to.equal(4) | ||
612 | expect(videoElements[3].startTimestamp).to.be.null | ||
613 | expect(videoElements[3].stopTimestamp).to.equal(35) | ||
614 | |||
615 | expect(videoElements[4].video.name).to.equal('video 4 server 1') | ||
616 | expect(videoElements[4].position).to.equal(5) | ||
617 | expect(videoElements[4].startTimestamp).to.equal(45) | ||
618 | expect(videoElements[4].stopTimestamp).to.equal(60) | ||
619 | |||
620 | expect(videoElements[5].video.name).to.equal('NSFW video') | ||
621 | expect(videoElements[5].position).to.equal(6) | ||
622 | expect(videoElements[5].startTimestamp).to.equal(5) | ||
623 | expect(videoElements[5].stopTimestamp).to.be.null | ||
624 | |||
625 | expect(videoElements[6].video.name).to.equal('NSFW video') | ||
626 | expect(videoElements[6].position).to.equal(7) | ||
627 | expect(videoElements[6].startTimestamp).to.equal(4) | ||
628 | expect(videoElements[6].stopTimestamp).to.be.null | ||
629 | |||
630 | expect(videoElements[7].video.name).to.equal('NSFW video') | ||
631 | expect(videoElements[7].position).to.equal(8) | ||
632 | expect(videoElements[7].startTimestamp).to.be.null | ||
633 | expect(videoElements[7].stopTimestamp).to.be.null | ||
634 | } | ||
635 | |||
636 | { | ||
637 | const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 2 }) | ||
638 | expect(body.data).to.have.lengthOf(2) | ||
639 | } | ||
640 | } | ||
641 | }) | ||
642 | }) | ||
643 | |||
644 | describe('Element type', function () { | ||
645 | let groupUser1: PeerTubeServer[] | ||
646 | let groupWithoutToken1: PeerTubeServer[] | ||
647 | let group1: PeerTubeServer[] | ||
648 | let group2: PeerTubeServer[] | ||
649 | |||
650 | let video1: string | ||
651 | let video2: string | ||
652 | let video3: string | ||
653 | |||
654 | before(async function () { | ||
655 | this.timeout(60000) | ||
656 | |||
657 | groupUser1 = [ Object.assign({}, servers[0], { accessToken: userTokenServer1 }) ] | ||
658 | groupWithoutToken1 = [ Object.assign({}, servers[0], { accessToken: undefined }) ] | ||
659 | group1 = [ servers[0] ] | ||
660 | group2 = [ servers[1], servers[2] ] | ||
661 | |||
662 | const playlist = await commands[0].create({ | ||
663 | token: userTokenServer1, | ||
664 | attributes: { | ||
665 | displayName: 'playlist 56', | ||
666 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
667 | videoChannelId: servers[0].store.channel.id | ||
668 | } | ||
669 | }) | ||
670 | |||
671 | const playlistServer1Id2 = playlist.id | ||
672 | playlistServer1UUID2 = playlist.uuid | ||
673 | |||
674 | const addVideo = (attributes: any) => { | ||
675 | return commands[0].addElement({ token: userTokenServer1, playlistId: playlistServer1Id2, attributes }) | ||
676 | } | ||
677 | |||
678 | video1 = (await servers[0].videos.quickUpload({ name: 'video 89', token: userTokenServer1 })).uuid | ||
679 | video2 = (await servers[1].videos.quickUpload({ name: 'video 90' })).uuid | ||
680 | video3 = (await servers[0].videos.quickUpload({ name: 'video 91', nsfw: true })).uuid | ||
681 | |||
682 | await waitJobs(servers) | ||
683 | |||
684 | await addVideo({ videoId: video1, startTimestamp: 15, stopTimestamp: 28 }) | ||
685 | await addVideo({ videoId: video2, startTimestamp: 35 }) | ||
686 | await addVideo({ videoId: video3 }) | ||
687 | |||
688 | await waitJobs(servers) | ||
689 | }) | ||
690 | |||
691 | it('Should update the element type if the video is private/password protected', async function () { | ||
692 | this.timeout(20000) | ||
693 | |||
694 | const name = 'video 89' | ||
695 | const position = 1 | ||
696 | |||
697 | { | ||
698 | await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PRIVATE } }) | ||
699 | await waitJobs(servers) | ||
700 | |||
701 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
702 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
703 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
704 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
705 | } | ||
706 | |||
707 | { | ||
708 | await servers[0].videos.update({ | ||
709 | id: video1, | ||
710 | attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] } | ||
711 | }) | ||
712 | await waitJobs(servers) | ||
713 | |||
714 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
715 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
716 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
717 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
718 | } | ||
719 | |||
720 | { | ||
721 | await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
722 | await waitJobs(servers) | ||
723 | |||
724 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
725 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
726 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
727 | // We deleted the video, so even if we recreated it, the old entry is still deleted | ||
728 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
729 | } | ||
730 | }) | ||
731 | |||
732 | it('Should update the element type if the video is blacklisted', async function () { | ||
733 | this.timeout(20000) | ||
734 | |||
735 | const name = 'video 89' | ||
736 | const position = 1 | ||
737 | |||
738 | { | ||
739 | await servers[0].blacklist.add({ videoId: video1, reason: 'reason', unfederate: true }) | ||
740 | await waitJobs(servers) | ||
741 | |||
742 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
743 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) | ||
744 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) | ||
745 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
746 | } | ||
747 | |||
748 | { | ||
749 | await servers[0].blacklist.remove({ videoId: video1 }) | ||
750 | await waitJobs(servers) | ||
751 | |||
752 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
753 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
754 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
755 | // We deleted the video (because unfederated), so even if we recreated it, the old entry is still deleted | ||
756 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
757 | } | ||
758 | }) | ||
759 | |||
760 | it('Should update the element type if the account or server of the video is blocked', async function () { | ||
761 | this.timeout(90000) | ||
762 | |||
763 | const command = servers[0].blocklist | ||
764 | |||
765 | const name = 'video 90' | ||
766 | const position = 2 | ||
767 | |||
768 | { | ||
769 | await command.addToMyBlocklist({ token: userTokenServer1, account: 'root@' + servers[1].host }) | ||
770 | await waitJobs(servers) | ||
771 | |||
772 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) | ||
773 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
774 | |||
775 | await command.removeFromMyBlocklist({ token: userTokenServer1, account: 'root@' + servers[1].host }) | ||
776 | await waitJobs(servers) | ||
777 | |||
778 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
779 | } | ||
780 | |||
781 | { | ||
782 | await command.addToMyBlocklist({ token: userTokenServer1, server: servers[1].host }) | ||
783 | await waitJobs(servers) | ||
784 | |||
785 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) | ||
786 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
787 | |||
788 | await command.removeFromMyBlocklist({ token: userTokenServer1, server: servers[1].host }) | ||
789 | await waitJobs(servers) | ||
790 | |||
791 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
792 | } | ||
793 | |||
794 | { | ||
795 | await command.addToServerBlocklist({ account: 'root@' + servers[1].host }) | ||
796 | await waitJobs(servers) | ||
797 | |||
798 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) | ||
799 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
800 | |||
801 | await command.removeFromServerBlocklist({ account: 'root@' + servers[1].host }) | ||
802 | await waitJobs(servers) | ||
803 | |||
804 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
805 | } | ||
806 | |||
807 | { | ||
808 | await command.addToServerBlocklist({ server: servers[1].host }) | ||
809 | await waitJobs(servers) | ||
810 | |||
811 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) | ||
812 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
813 | |||
814 | await command.removeFromServerBlocklist({ server: servers[1].host }) | ||
815 | await waitJobs(servers) | ||
816 | |||
817 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
818 | } | ||
819 | }) | ||
820 | }) | ||
821 | |||
822 | describe('Managing playlist elements', function () { | ||
823 | |||
824 | it('Should reorder the playlist', async function () { | ||
825 | this.timeout(30000) | ||
826 | |||
827 | { | ||
828 | await commands[0].reorderElements({ | ||
829 | playlistId: playlistServer1Id, | ||
830 | attributes: { | ||
831 | startPosition: 2, | ||
832 | insertAfterPosition: 3 | ||
833 | } | ||
834 | }) | ||
835 | |||
836 | await waitJobs(servers) | ||
837 | |||
838 | for (const server of servers) { | ||
839 | const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 }) | ||
840 | const names = body.data.map(v => v.video.name) | ||
841 | |||
842 | expect(names).to.deep.equal([ | ||
843 | 'video 0 server 1', | ||
844 | 'video 2 server 3', | ||
845 | 'video 1 server 3', | ||
846 | 'video 3 server 1', | ||
847 | 'video 4 server 1', | ||
848 | 'NSFW video', | ||
849 | 'NSFW video', | ||
850 | 'NSFW video' | ||
851 | ]) | ||
852 | } | ||
853 | } | ||
854 | |||
855 | { | ||
856 | await commands[0].reorderElements({ | ||
857 | playlistId: playlistServer1Id, | ||
858 | attributes: { | ||
859 | startPosition: 1, | ||
860 | reorderLength: 3, | ||
861 | insertAfterPosition: 4 | ||
862 | } | ||
863 | }) | ||
864 | |||
865 | await waitJobs(servers) | ||
866 | |||
867 | for (const server of servers) { | ||
868 | const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 }) | ||
869 | const names = body.data.map(v => v.video.name) | ||
870 | |||
871 | expect(names).to.deep.equal([ | ||
872 | 'video 3 server 1', | ||
873 | 'video 0 server 1', | ||
874 | 'video 2 server 3', | ||
875 | 'video 1 server 3', | ||
876 | 'video 4 server 1', | ||
877 | 'NSFW video', | ||
878 | 'NSFW video', | ||
879 | 'NSFW video' | ||
880 | ]) | ||
881 | } | ||
882 | } | ||
883 | |||
884 | { | ||
885 | await commands[0].reorderElements({ | ||
886 | playlistId: playlistServer1Id, | ||
887 | attributes: { | ||
888 | startPosition: 6, | ||
889 | insertAfterPosition: 3 | ||
890 | } | ||
891 | }) | ||
892 | |||
893 | await waitJobs(servers) | ||
894 | |||
895 | for (const server of servers) { | ||
896 | const { data: elements } = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 }) | ||
897 | const names = elements.map(v => v.video.name) | ||
898 | |||
899 | expect(names).to.deep.equal([ | ||
900 | 'video 3 server 1', | ||
901 | 'video 0 server 1', | ||
902 | 'video 2 server 3', | ||
903 | 'NSFW video', | ||
904 | 'video 1 server 3', | ||
905 | 'video 4 server 1', | ||
906 | 'NSFW video', | ||
907 | 'NSFW video' | ||
908 | ]) | ||
909 | |||
910 | for (let i = 1; i <= elements.length; i++) { | ||
911 | expect(elements[i - 1].position).to.equal(i) | ||
912 | } | ||
913 | } | ||
914 | } | ||
915 | }) | ||
916 | |||
917 | it('Should update startTimestamp/endTimestamp of some elements', async function () { | ||
918 | this.timeout(30000) | ||
919 | |||
920 | await commands[0].updateElement({ | ||
921 | playlistId: playlistServer1Id, | ||
922 | elementId: playlistElementServer1Video4, | ||
923 | attributes: { | ||
924 | startTimestamp: 1 | ||
925 | } | ||
926 | }) | ||
927 | |||
928 | await commands[0].updateElement({ | ||
929 | playlistId: playlistServer1Id, | ||
930 | elementId: playlistElementServer1Video5, | ||
931 | attributes: { | ||
932 | stopTimestamp: null | ||
933 | } | ||
934 | }) | ||
935 | |||
936 | await waitJobs(servers) | ||
937 | |||
938 | for (const server of servers) { | ||
939 | const { data: elements } = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 }) | ||
940 | |||
941 | expect(elements[0].video.name).to.equal('video 3 server 1') | ||
942 | expect(elements[0].position).to.equal(1) | ||
943 | expect(elements[0].startTimestamp).to.equal(1) | ||
944 | expect(elements[0].stopTimestamp).to.equal(35) | ||
945 | |||
946 | expect(elements[5].video.name).to.equal('video 4 server 1') | ||
947 | expect(elements[5].position).to.equal(6) | ||
948 | expect(elements[5].startTimestamp).to.equal(45) | ||
949 | expect(elements[5].stopTimestamp).to.be.null | ||
950 | } | ||
951 | }) | ||
952 | |||
953 | it('Should check videos existence in my playlist', async function () { | ||
954 | const videoIds = [ | ||
955 | servers[0].store.videos[0].id, | ||
956 | 42000, | ||
957 | servers[0].store.videos[3].id, | ||
958 | 43000, | ||
959 | servers[0].store.videos[4].id | ||
960 | ] | ||
961 | const obj = await commands[0].videosExist({ videoIds }) | ||
962 | |||
963 | { | ||
964 | const elem = obj[servers[0].store.videos[0].id] | ||
965 | expect(elem).to.have.lengthOf(1) | ||
966 | expect(elem[0].playlistElementId).to.exist | ||
967 | expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName) | ||
968 | expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID)) | ||
969 | expect(elem[0].playlistId).to.equal(playlistServer1Id) | ||
970 | expect(elem[0].startTimestamp).to.equal(15) | ||
971 | expect(elem[0].stopTimestamp).to.equal(28) | ||
972 | } | ||
973 | |||
974 | { | ||
975 | const elem = obj[servers[0].store.videos[3].id] | ||
976 | expect(elem).to.have.lengthOf(1) | ||
977 | expect(elem[0].playlistElementId).to.equal(playlistElementServer1Video4) | ||
978 | expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName) | ||
979 | expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID)) | ||
980 | expect(elem[0].playlistId).to.equal(playlistServer1Id) | ||
981 | expect(elem[0].startTimestamp).to.equal(1) | ||
982 | expect(elem[0].stopTimestamp).to.equal(35) | ||
983 | } | ||
984 | |||
985 | { | ||
986 | const elem = obj[servers[0].store.videos[4].id] | ||
987 | expect(elem).to.have.lengthOf(1) | ||
988 | expect(elem[0].playlistId).to.equal(playlistServer1Id) | ||
989 | expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName) | ||
990 | expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID)) | ||
991 | expect(elem[0].startTimestamp).to.equal(45) | ||
992 | expect(elem[0].stopTimestamp).to.equal(null) | ||
993 | } | ||
994 | |||
995 | expect(obj[42000]).to.have.lengthOf(0) | ||
996 | expect(obj[43000]).to.have.lengthOf(0) | ||
997 | }) | ||
998 | |||
999 | it('Should automatically update updatedAt field of playlists', async function () { | ||
1000 | const server = servers[1] | ||
1001 | const videoId = servers[1].store.videos[5].id | ||
1002 | |||
1003 | async function getPlaylistNames () { | ||
1004 | const { data } = await server.playlists.listByAccount({ token: server.accessToken, handle: 'root', sort: '-updatedAt' }) | ||
1005 | |||
1006 | return data.map(p => p.displayName) | ||
1007 | } | ||
1008 | |||
1009 | const attributes = { videoId } | ||
1010 | const element1 = await server.playlists.addElement({ playlistId: playlistServer2Id1, attributes }) | ||
1011 | const element2 = await server.playlists.addElement({ playlistId: playlistServer2Id2, attributes }) | ||
1012 | |||
1013 | const names1 = await getPlaylistNames() | ||
1014 | expect(names1[0]).to.equal('playlist 3 updated') | ||
1015 | expect(names1[1]).to.equal('playlist 2') | ||
1016 | |||
1017 | await server.playlists.removeElement({ playlistId: playlistServer2Id1, elementId: element1.id }) | ||
1018 | |||
1019 | const names2 = await getPlaylistNames() | ||
1020 | expect(names2[0]).to.equal('playlist 2') | ||
1021 | expect(names2[1]).to.equal('playlist 3 updated') | ||
1022 | |||
1023 | await server.playlists.removeElement({ playlistId: playlistServer2Id2, elementId: element2.id }) | ||
1024 | |||
1025 | const names3 = await getPlaylistNames() | ||
1026 | expect(names3[0]).to.equal('playlist 3 updated') | ||
1027 | expect(names3[1]).to.equal('playlist 2') | ||
1028 | }) | ||
1029 | |||
1030 | it('Should delete some elements', async function () { | ||
1031 | this.timeout(30000) | ||
1032 | |||
1033 | await commands[0].removeElement({ playlistId: playlistServer1Id, elementId: playlistElementServer1Video4 }) | ||
1034 | await commands[0].removeElement({ playlistId: playlistServer1Id, elementId: playlistElementNSFW }) | ||
1035 | |||
1036 | await waitJobs(servers) | ||
1037 | |||
1038 | for (const server of servers) { | ||
1039 | const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 }) | ||
1040 | expect(body.total).to.equal(6) | ||
1041 | |||
1042 | const elements = body.data | ||
1043 | expect(elements).to.have.lengthOf(6) | ||
1044 | |||
1045 | expect(elements[0].video.name).to.equal('video 0 server 1') | ||
1046 | expect(elements[0].position).to.equal(1) | ||
1047 | |||
1048 | expect(elements[1].video.name).to.equal('video 2 server 3') | ||
1049 | expect(elements[1].position).to.equal(2) | ||
1050 | |||
1051 | expect(elements[2].video.name).to.equal('video 1 server 3') | ||
1052 | expect(elements[2].position).to.equal(3) | ||
1053 | |||
1054 | expect(elements[3].video.name).to.equal('video 4 server 1') | ||
1055 | expect(elements[3].position).to.equal(4) | ||
1056 | |||
1057 | expect(elements[4].video.name).to.equal('NSFW video') | ||
1058 | expect(elements[4].position).to.equal(5) | ||
1059 | |||
1060 | expect(elements[5].video.name).to.equal('NSFW video') | ||
1061 | expect(elements[5].position).to.equal(6) | ||
1062 | } | ||
1063 | }) | ||
1064 | |||
1065 | it('Should be able to create a public playlist, and set it to private', async function () { | ||
1066 | this.timeout(30000) | ||
1067 | |||
1068 | const videoPlaylistIds = await commands[0].create({ | ||
1069 | attributes: { | ||
1070 | displayName: 'my super public playlist', | ||
1071 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
1072 | videoChannelId: servers[0].store.channel.id | ||
1073 | } | ||
1074 | }) | ||
1075 | |||
1076 | await waitJobs(servers) | ||
1077 | |||
1078 | for (const server of servers) { | ||
1079 | await server.playlists.get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.OK_200 }) | ||
1080 | } | ||
1081 | |||
1082 | const attributes = { privacy: VideoPlaylistPrivacy.PRIVATE } | ||
1083 | await commands[0].update({ playlistId: videoPlaylistIds.id, attributes }) | ||
1084 | |||
1085 | await waitJobs(servers) | ||
1086 | |||
1087 | for (const server of [ servers[1], servers[2] ]) { | ||
1088 | await server.playlists.get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
1089 | } | ||
1090 | |||
1091 | await commands[0].get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
1092 | await commands[0].get({ token: servers[0].accessToken, playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.OK_200 }) | ||
1093 | }) | ||
1094 | }) | ||
1095 | |||
1096 | describe('Playlist deletion', function () { | ||
1097 | |||
1098 | it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { | ||
1099 | this.timeout(30000) | ||
1100 | |||
1101 | await commands[0].delete({ playlistId: playlistServer1Id }) | ||
1102 | |||
1103 | await waitJobs(servers) | ||
1104 | |||
1105 | for (const server of servers) { | ||
1106 | await server.playlists.get({ playlistId: playlistServer1UUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
1107 | } | ||
1108 | }) | ||
1109 | |||
1110 | it('Should have deleted the thumbnail on server 1, 2 and 3', async function () { | ||
1111 | this.timeout(30000) | ||
1112 | |||
1113 | for (const server of servers) { | ||
1114 | await checkPlaylistFilesWereRemoved(playlistServer1UUID, server) | ||
1115 | } | ||
1116 | }) | ||
1117 | |||
1118 | it('Should unfollow servers 1 and 2 and hide their playlists', async function () { | ||
1119 | this.timeout(30000) | ||
1120 | |||
1121 | const finder = (data: VideoPlaylist[]) => data.find(p => p.displayName === 'my super playlist') | ||
1122 | |||
1123 | { | ||
1124 | const body = await servers[2].playlists.list({ start: 0, count: 5 }) | ||
1125 | expect(body.total).to.equal(3) | ||
1126 | |||
1127 | expect(finder(body.data)).to.not.be.undefined | ||
1128 | } | ||
1129 | |||
1130 | await servers[2].follows.unfollow({ target: servers[0] }) | ||
1131 | |||
1132 | { | ||
1133 | const body = await servers[2].playlists.list({ start: 0, count: 5 }) | ||
1134 | expect(body.total).to.equal(1) | ||
1135 | |||
1136 | expect(finder(body.data)).to.be.undefined | ||
1137 | } | ||
1138 | }) | ||
1139 | |||
1140 | it('Should delete a channel and put the associated playlist in private mode', async function () { | ||
1141 | this.timeout(30000) | ||
1142 | |||
1143 | const channel = await servers[0].channels.create({ attributes: { name: 'super_channel', displayName: 'super channel' } }) | ||
1144 | |||
1145 | const playlistCreated = await commands[0].create({ | ||
1146 | attributes: { | ||
1147 | displayName: 'channel playlist', | ||
1148 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
1149 | videoChannelId: channel.id | ||
1150 | } | ||
1151 | }) | ||
1152 | |||
1153 | await waitJobs(servers) | ||
1154 | |||
1155 | await servers[0].channels.delete({ channelName: 'super_channel' }) | ||
1156 | |||
1157 | await waitJobs(servers) | ||
1158 | |||
1159 | const body = await commands[0].get({ token: servers[0].accessToken, playlistId: playlistCreated.uuid }) | ||
1160 | expect(body.displayName).to.equal('channel playlist') | ||
1161 | expect(body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) | ||
1162 | |||
1163 | await servers[1].playlists.get({ playlistId: playlistCreated.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
1164 | }) | ||
1165 | |||
1166 | it('Should delete an account and delete its playlists', async function () { | ||
1167 | this.timeout(30000) | ||
1168 | |||
1169 | const { userId, token } = await servers[0].users.generate('user_1') | ||
1170 | |||
1171 | const { videoChannels } = await servers[0].users.getMyInfo({ token }) | ||
1172 | const userChannel = videoChannels[0] | ||
1173 | |||
1174 | await commands[0].create({ | ||
1175 | attributes: { | ||
1176 | displayName: 'playlist to be deleted', | ||
1177 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
1178 | videoChannelId: userChannel.id | ||
1179 | } | ||
1180 | }) | ||
1181 | |||
1182 | await waitJobs(servers) | ||
1183 | |||
1184 | const finder = (data: VideoPlaylist[]) => data.find(p => p.displayName === 'playlist to be deleted') | ||
1185 | |||
1186 | { | ||
1187 | for (const server of [ servers[0], servers[1] ]) { | ||
1188 | const body = await server.playlists.list({ start: 0, count: 15 }) | ||
1189 | |||
1190 | expect(finder(body.data)).to.not.be.undefined | ||
1191 | } | ||
1192 | } | ||
1193 | |||
1194 | await servers[0].users.remove({ userId }) | ||
1195 | await waitJobs(servers) | ||
1196 | |||
1197 | { | ||
1198 | for (const server of [ servers[0], servers[1] ]) { | ||
1199 | const body = await server.playlists.list({ start: 0, count: 15 }) | ||
1200 | |||
1201 | expect(finder(body.data)).to.be.undefined | ||
1202 | } | ||
1203 | } | ||
1204 | }) | ||
1205 | }) | ||
1206 | |||
1207 | after(async function () { | ||
1208 | await cleanupTests(servers) | ||
1209 | }) | ||
1210 | }) | ||
diff --git a/packages/tests/src/api/videos/video-privacy.ts b/packages/tests/src/api/videos/video-privacy.ts new file mode 100644 index 000000000..9171463a4 --- /dev/null +++ b/packages/tests/src/api/videos/video-privacy.ts | |||
@@ -0,0 +1,294 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('Test video privacy', function () { | ||
16 | const servers: PeerTubeServer[] = [] | ||
17 | let anotherUserToken: string | ||
18 | |||
19 | let privateVideoId: number | ||
20 | let privateVideoUUID: string | ||
21 | |||
22 | let internalVideoId: number | ||
23 | let internalVideoUUID: string | ||
24 | |||
25 | let unlistedVideo: VideoCreateResult | ||
26 | let nonFederatedUnlistedVideoUUID: string | ||
27 | |||
28 | let now: number | ||
29 | |||
30 | const dontFederateUnlistedConfig = { | ||
31 | federation: { | ||
32 | videos: { | ||
33 | federate_unlisted: false | ||
34 | } | ||
35 | } | ||
36 | } | ||
37 | |||
38 | before(async function () { | ||
39 | this.timeout(50000) | ||
40 | |||
41 | // Run servers | ||
42 | servers.push(await createSingleServer(1, dontFederateUnlistedConfig)) | ||
43 | servers.push(await createSingleServer(2)) | ||
44 | |||
45 | // Get the access tokens | ||
46 | await setAccessTokensToServers(servers) | ||
47 | |||
48 | // Server 1 and server 2 follow each other | ||
49 | await doubleFollow(servers[0], servers[1]) | ||
50 | }) | ||
51 | |||
52 | describe('Private and internal videos', function () { | ||
53 | |||
54 | it('Should upload a private and internal videos on server 1', async function () { | ||
55 | this.timeout(50000) | ||
56 | |||
57 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | ||
58 | const attributes = { privacy } | ||
59 | await servers[0].videos.upload({ attributes }) | ||
60 | } | ||
61 | |||
62 | await waitJobs(servers) | ||
63 | }) | ||
64 | |||
65 | it('Should not have these private and internal videos on server 2', async function () { | ||
66 | const { total, data } = await servers[1].videos.list() | ||
67 | |||
68 | expect(total).to.equal(0) | ||
69 | expect(data).to.have.lengthOf(0) | ||
70 | }) | ||
71 | |||
72 | it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () { | ||
73 | const { total, data } = await servers[0].videos.list() | ||
74 | |||
75 | expect(total).to.equal(0) | ||
76 | expect(data).to.have.lengthOf(0) | ||
77 | }) | ||
78 | |||
79 | it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () { | ||
80 | const { total, data } = await servers[0].videos.listWithToken() | ||
81 | |||
82 | expect(total).to.equal(1) | ||
83 | expect(data).to.have.lengthOf(1) | ||
84 | |||
85 | expect(data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL) | ||
86 | }) | ||
87 | |||
88 | it('Should list my (private and internal) videos', async function () { | ||
89 | const { total, data } = await servers[0].videos.listMyVideos() | ||
90 | |||
91 | expect(total).to.equal(2) | ||
92 | expect(data).to.have.lengthOf(2) | ||
93 | |||
94 | const privateVideo = data.find(v => v.privacy.id === VideoPrivacy.PRIVATE) | ||
95 | privateVideoId = privateVideo.id | ||
96 | privateVideoUUID = privateVideo.uuid | ||
97 | |||
98 | const internalVideo = data.find(v => v.privacy.id === VideoPrivacy.INTERNAL) | ||
99 | internalVideoId = internalVideo.id | ||
100 | internalVideoUUID = internalVideo.uuid | ||
101 | }) | ||
102 | |||
103 | it('Should not be able to watch the private/internal video with non authenticated user', async function () { | ||
104 | await servers[0].videos.get({ id: privateVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
105 | await servers[0].videos.get({ id: internalVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
106 | }) | ||
107 | |||
108 | it('Should not be able to watch the private video with another user', async function () { | ||
109 | const user = { | ||
110 | username: 'hello', | ||
111 | password: 'super password' | ||
112 | } | ||
113 | await servers[0].users.create({ username: user.username, password: user.password }) | ||
114 | |||
115 | anotherUserToken = await servers[0].login.getAccessToken(user) | ||
116 | |||
117 | await servers[0].videos.getWithToken({ | ||
118 | token: anotherUserToken, | ||
119 | id: privateVideoUUID, | ||
120 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | it('Should be able to watch the internal video with another user', async function () { | ||
125 | await servers[0].videos.getWithToken({ token: anotherUserToken, id: internalVideoUUID }) | ||
126 | }) | ||
127 | |||
128 | it('Should be able to watch the private video with the correct user', async function () { | ||
129 | await servers[0].videos.getWithToken({ id: privateVideoUUID }) | ||
130 | }) | ||
131 | }) | ||
132 | |||
133 | describe('Unlisted videos', function () { | ||
134 | |||
135 | it('Should upload an unlisted video on server 2', async function () { | ||
136 | this.timeout(120000) | ||
137 | |||
138 | const attributes = { | ||
139 | name: 'unlisted video', | ||
140 | privacy: VideoPrivacy.UNLISTED | ||
141 | } | ||
142 | await servers[1].videos.upload({ attributes }) | ||
143 | |||
144 | // Server 2 has transcoding enabled | ||
145 | await waitJobs(servers) | ||
146 | }) | ||
147 | |||
148 | it('Should not have this unlisted video listed on server 1 and 2', async function () { | ||
149 | for (const server of servers) { | ||
150 | const { total, data } = await server.videos.list() | ||
151 | |||
152 | expect(total).to.equal(0) | ||
153 | expect(data).to.have.lengthOf(0) | ||
154 | } | ||
155 | }) | ||
156 | |||
157 | it('Should list my (unlisted) videos', async function () { | ||
158 | const { total, data } = await servers[1].videos.listMyVideos() | ||
159 | |||
160 | expect(total).to.equal(1) | ||
161 | expect(data).to.have.lengthOf(1) | ||
162 | |||
163 | unlistedVideo = data[0] | ||
164 | }) | ||
165 | |||
166 | it('Should not be able to get this unlisted video using its id', async function () { | ||
167 | await servers[1].videos.get({ id: unlistedVideo.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
168 | }) | ||
169 | |||
170 | it('Should be able to get this unlisted video using its uuid/shortUUID', async function () { | ||
171 | for (const server of servers) { | ||
172 | for (const id of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) { | ||
173 | const video = await server.videos.get({ id }) | ||
174 | |||
175 | expect(video.name).to.equal('unlisted video') | ||
176 | } | ||
177 | } | ||
178 | }) | ||
179 | |||
180 | it('Should upload a non-federating unlisted video to server 1', async function () { | ||
181 | this.timeout(30000) | ||
182 | |||
183 | const attributes = { | ||
184 | name: 'unlisted video', | ||
185 | privacy: VideoPrivacy.UNLISTED | ||
186 | } | ||
187 | await servers[0].videos.upload({ attributes }) | ||
188 | |||
189 | await waitJobs(servers) | ||
190 | }) | ||
191 | |||
192 | it('Should list my new unlisted video', async function () { | ||
193 | const { total, data } = await servers[0].videos.listMyVideos() | ||
194 | |||
195 | expect(total).to.equal(3) | ||
196 | expect(data).to.have.lengthOf(3) | ||
197 | |||
198 | nonFederatedUnlistedVideoUUID = data[0].uuid | ||
199 | }) | ||
200 | |||
201 | it('Should be able to get non-federated unlisted video from origin', async function () { | ||
202 | const video = await servers[0].videos.get({ id: nonFederatedUnlistedVideoUUID }) | ||
203 | |||
204 | expect(video.name).to.equal('unlisted video') | ||
205 | }) | ||
206 | |||
207 | it('Should not be able to get non-federated unlisted video from federated server', async function () { | ||
208 | await servers[1].videos.get({ id: nonFederatedUnlistedVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
209 | }) | ||
210 | }) | ||
211 | |||
212 | describe('Privacy update', function () { | ||
213 | |||
214 | it('Should update the private and internal videos to public on server 1', async function () { | ||
215 | this.timeout(100000) | ||
216 | |||
217 | now = Date.now() | ||
218 | |||
219 | { | ||
220 | const attributes = { | ||
221 | name: 'private video becomes public', | ||
222 | privacy: VideoPrivacy.PUBLIC | ||
223 | } | ||
224 | |||
225 | await servers[0].videos.update({ id: privateVideoId, attributes }) | ||
226 | } | ||
227 | |||
228 | { | ||
229 | const attributes = { | ||
230 | name: 'internal video becomes public', | ||
231 | privacy: VideoPrivacy.PUBLIC | ||
232 | } | ||
233 | await servers[0].videos.update({ id: internalVideoId, attributes }) | ||
234 | } | ||
235 | |||
236 | await wait(10000) | ||
237 | await waitJobs(servers) | ||
238 | }) | ||
239 | |||
240 | it('Should have this new public video listed on server 1 and 2', async function () { | ||
241 | for (const server of servers) { | ||
242 | const { total, data } = await server.videos.list() | ||
243 | expect(total).to.equal(2) | ||
244 | expect(data).to.have.lengthOf(2) | ||
245 | |||
246 | const privateVideo = data.find(v => v.name === 'private video becomes public') | ||
247 | const internalVideo = data.find(v => v.name === 'internal video becomes public') | ||
248 | |||
249 | expect(privateVideo).to.not.be.undefined | ||
250 | expect(internalVideo).to.not.be.undefined | ||
251 | |||
252 | expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now) | ||
253 | // We don't change the publish date of internal videos | ||
254 | expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now) | ||
255 | |||
256 | expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) | ||
257 | expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) | ||
258 | } | ||
259 | }) | ||
260 | |||
261 | it('Should set these videos as private and internal', async function () { | ||
262 | await servers[0].videos.update({ id: internalVideoId, attributes: { privacy: VideoPrivacy.PRIVATE } }) | ||
263 | await servers[0].videos.update({ id: privateVideoId, attributes: { privacy: VideoPrivacy.INTERNAL } }) | ||
264 | |||
265 | await waitJobs(servers) | ||
266 | |||
267 | for (const server of servers) { | ||
268 | const { total, data } = await server.videos.list() | ||
269 | |||
270 | expect(total).to.equal(0) | ||
271 | expect(data).to.have.lengthOf(0) | ||
272 | } | ||
273 | |||
274 | { | ||
275 | const { total, data } = await servers[0].videos.listMyVideos() | ||
276 | expect(total).to.equal(3) | ||
277 | expect(data).to.have.lengthOf(3) | ||
278 | |||
279 | const privateVideo = data.find(v => v.name === 'private video becomes public') | ||
280 | const internalVideo = data.find(v => v.name === 'internal video becomes public') | ||
281 | |||
282 | expect(privateVideo).to.not.be.undefined | ||
283 | expect(internalVideo).to.not.be.undefined | ||
284 | |||
285 | expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL) | ||
286 | expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE) | ||
287 | } | ||
288 | }) | ||
289 | }) | ||
290 | |||
291 | after(async function () { | ||
292 | await cleanupTests(servers) | ||
293 | }) | ||
294 | }) | ||
diff --git a/packages/tests/src/api/videos/video-schedule-update.ts b/packages/tests/src/api/videos/video-schedule-update.ts new file mode 100644 index 000000000..96d71933e --- /dev/null +++ b/packages/tests/src/api/videos/video-schedule-update.ts | |||
@@ -0,0 +1,155 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | function in10Seconds () { | ||
16 | const now = new Date() | ||
17 | now.setSeconds(now.getSeconds() + 10) | ||
18 | |||
19 | return now | ||
20 | } | ||
21 | |||
22 | describe('Test video update scheduler', function () { | ||
23 | let servers: PeerTubeServer[] = [] | ||
24 | let video2UUID: string | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(30000) | ||
28 | |||
29 | // Run servers | ||
30 | servers = await createMultipleServers(2) | ||
31 | |||
32 | await setAccessTokensToServers(servers) | ||
33 | |||
34 | await doubleFollow(servers[0], servers[1]) | ||
35 | }) | ||
36 | |||
37 | it('Should upload a video and schedule an update in 10 seconds', async function () { | ||
38 | const attributes = { | ||
39 | name: 'video 1', | ||
40 | privacy: VideoPrivacy.PRIVATE, | ||
41 | scheduleUpdate: { | ||
42 | updateAt: in10Seconds().toISOString(), | ||
43 | privacy: VideoPrivacy.PUBLIC | ||
44 | } | ||
45 | } | ||
46 | |||
47 | await servers[0].videos.upload({ attributes }) | ||
48 | |||
49 | await waitJobs(servers) | ||
50 | }) | ||
51 | |||
52 | it('Should not list the video (in privacy mode)', async function () { | ||
53 | for (const server of servers) { | ||
54 | const { total } = await server.videos.list() | ||
55 | |||
56 | expect(total).to.equal(0) | ||
57 | } | ||
58 | }) | ||
59 | |||
60 | it('Should have my scheduled video in my account videos', async function () { | ||
61 | const { total, data } = await servers[0].videos.listMyVideos() | ||
62 | expect(total).to.equal(1) | ||
63 | |||
64 | const videoFromList = data[0] | ||
65 | const videoFromGet = await servers[0].videos.getWithToken({ id: videoFromList.uuid }) | ||
66 | |||
67 | for (const video of [ videoFromList, videoFromGet ]) { | ||
68 | expect(video.name).to.equal('video 1') | ||
69 | expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE) | ||
70 | expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date()) | ||
71 | expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
72 | } | ||
73 | }) | ||
74 | |||
75 | it('Should wait some seconds and have the video in public privacy', async function () { | ||
76 | this.timeout(50000) | ||
77 | |||
78 | await wait(15000) | ||
79 | await waitJobs(servers) | ||
80 | |||
81 | for (const server of servers) { | ||
82 | const { total, data } = await server.videos.list() | ||
83 | |||
84 | expect(total).to.equal(1) | ||
85 | expect(data[0].name).to.equal('video 1') | ||
86 | } | ||
87 | }) | ||
88 | |||
89 | it('Should upload a video without scheduling an update', async function () { | ||
90 | const attributes = { | ||
91 | name: 'video 2', | ||
92 | privacy: VideoPrivacy.PRIVATE | ||
93 | } | ||
94 | |||
95 | const { uuid } = await servers[0].videos.upload({ attributes }) | ||
96 | video2UUID = uuid | ||
97 | |||
98 | await waitJobs(servers) | ||
99 | }) | ||
100 | |||
101 | it('Should update a video by scheduling an update', async function () { | ||
102 | const attributes = { | ||
103 | name: 'video 2 updated', | ||
104 | scheduleUpdate: { | ||
105 | updateAt: in10Seconds().toISOString(), | ||
106 | privacy: VideoPrivacy.PUBLIC | ||
107 | } | ||
108 | } | ||
109 | |||
110 | await servers[0].videos.update({ id: video2UUID, attributes }) | ||
111 | await waitJobs(servers) | ||
112 | }) | ||
113 | |||
114 | it('Should not display the updated video', async function () { | ||
115 | for (const server of servers) { | ||
116 | const { total } = await server.videos.list() | ||
117 | |||
118 | expect(total).to.equal(1) | ||
119 | } | ||
120 | }) | ||
121 | |||
122 | it('Should have my scheduled updated video in my account videos', async function () { | ||
123 | const { total, data } = await servers[0].videos.listMyVideos() | ||
124 | expect(total).to.equal(2) | ||
125 | |||
126 | const video = data.find(v => v.uuid === video2UUID) | ||
127 | expect(video).not.to.be.undefined | ||
128 | |||
129 | expect(video.name).to.equal('video 2 updated') | ||
130 | expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE) | ||
131 | |||
132 | expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date()) | ||
133 | expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC) | ||
134 | }) | ||
135 | |||
136 | it('Should wait some seconds and have the updated video in public privacy', async function () { | ||
137 | this.timeout(20000) | ||
138 | |||
139 | await wait(15000) | ||
140 | await waitJobs(servers) | ||
141 | |||
142 | for (const server of servers) { | ||
143 | const { total, data } = await server.videos.list() | ||
144 | expect(total).to.equal(2) | ||
145 | |||
146 | const video = data.find(v => v.uuid === video2UUID) | ||
147 | expect(video).not.to.be.undefined | ||
148 | expect(video.name).to.equal('video 2 updated') | ||
149 | } | ||
150 | }) | ||
151 | |||
152 | after(async function () { | ||
153 | await cleanupTests(servers) | ||
154 | }) | ||
155 | }) | ||
diff --git a/packages/tests/src/api/videos/video-source.ts b/packages/tests/src/api/videos/video-source.ts new file mode 100644 index 000000000..efe8c3802 --- /dev/null +++ b/packages/tests/src/api/videos/video-source.ts | |||
@@ -0,0 +1,448 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { expect } from 'chai' | ||
3 | import { getAllFiles } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { expectStartWith } from '@tests/shared/checks.js' | ||
6 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | makeGetRequest, | ||
12 | makeRawRequest, | ||
13 | ObjectStorageCommand, | ||
14 | PeerTubeServer, | ||
15 | setAccessTokensToServers, | ||
16 | setDefaultAccountAvatar, | ||
17 | setDefaultVideoChannel, | ||
18 | waitJobs | ||
19 | } from '@peertube/peertube-server-commands' | ||
20 | |||
21 | describe('Test a video file replacement', function () { | ||
22 | let servers: PeerTubeServer[] = [] | ||
23 | |||
24 | let replaceDate: Date | ||
25 | let userToken: string | ||
26 | let uuid: string | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(50000) | ||
30 | |||
31 | servers = await createMultipleServers(2) | ||
32 | |||
33 | // Get the access tokens | ||
34 | await setAccessTokensToServers(servers) | ||
35 | await setDefaultVideoChannel(servers) | ||
36 | await setDefaultAccountAvatar(servers) | ||
37 | |||
38 | await servers[0].config.enableFileUpdate() | ||
39 | |||
40 | userToken = await servers[0].users.generateUserAndToken('user1') | ||
41 | |||
42 | // Server 1 and server 2 follow each other | ||
43 | await doubleFollow(servers[0], servers[1]) | ||
44 | }) | ||
45 | |||
46 | describe('Getting latest video source', () => { | ||
47 | const fixture = 'video_short.webm' | ||
48 | const uuids: string[] = [] | ||
49 | |||
50 | it('Should get the source filename with legacy upload', async function () { | ||
51 | this.timeout(30000) | ||
52 | |||
53 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' }) | ||
54 | uuids.push(uuid) | ||
55 | |||
56 | const source = await servers[0].videos.getSource({ id: uuid }) | ||
57 | expect(source.filename).to.equal(fixture) | ||
58 | }) | ||
59 | |||
60 | it('Should get the source filename with resumable upload', async function () { | ||
61 | this.timeout(30000) | ||
62 | |||
63 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' }) | ||
64 | uuids.push(uuid) | ||
65 | |||
66 | const source = await servers[0].videos.getSource({ id: uuid }) | ||
67 | expect(source.filename).to.equal(fixture) | ||
68 | }) | ||
69 | |||
70 | after(async function () { | ||
71 | this.timeout(60000) | ||
72 | |||
73 | for (const uuid of uuids) { | ||
74 | await servers[0].videos.remove({ id: uuid }) | ||
75 | } | ||
76 | |||
77 | await waitJobs(servers) | ||
78 | }) | ||
79 | }) | ||
80 | |||
81 | describe('Updating video source', function () { | ||
82 | |||
83 | describe('Filesystem', function () { | ||
84 | |||
85 | it('Should replace a video file with transcoding disabled', async function () { | ||
86 | this.timeout(120000) | ||
87 | |||
88 | await servers[0].config.disableTranscoding() | ||
89 | |||
90 | const { uuid } = await servers[0].videos.quickUpload({ name: 'fs without transcoding', fixture: 'video_short_720p.mp4' }) | ||
91 | await waitJobs(servers) | ||
92 | |||
93 | for (const server of servers) { | ||
94 | const video = await server.videos.get({ id: uuid }) | ||
95 | |||
96 | const files = getAllFiles(video) | ||
97 | expect(files).to.have.lengthOf(1) | ||
98 | expect(files[0].resolution.id).to.equal(720) | ||
99 | } | ||
100 | |||
101 | await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' }) | ||
102 | await waitJobs(servers) | ||
103 | |||
104 | for (const server of servers) { | ||
105 | const video = await server.videos.get({ id: uuid }) | ||
106 | |||
107 | const files = getAllFiles(video) | ||
108 | expect(files).to.have.lengthOf(1) | ||
109 | expect(files[0].resolution.id).to.equal(360) | ||
110 | } | ||
111 | }) | ||
112 | |||
113 | it('Should replace a video file with transcoding enabled', async function () { | ||
114 | this.timeout(120000) | ||
115 | |||
116 | const previousPaths: string[] = [] | ||
117 | |||
118 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true }) | ||
119 | |||
120 | const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: 'video_short_720p.mp4' }) | ||
121 | uuid = videoUUID | ||
122 | |||
123 | await waitJobs(servers) | ||
124 | |||
125 | for (const server of servers) { | ||
126 | const video = await server.videos.get({ id: uuid }) | ||
127 | expect(video.inputFileUpdatedAt).to.be.null | ||
128 | |||
129 | const files = getAllFiles(video) | ||
130 | expect(files).to.have.lengthOf(6 * 2) | ||
131 | |||
132 | // Grab old paths to ensure we'll regenerate | ||
133 | |||
134 | previousPaths.push(video.previewPath) | ||
135 | previousPaths.push(video.thumbnailPath) | ||
136 | |||
137 | for (const file of files) { | ||
138 | previousPaths.push(file.fileUrl) | ||
139 | previousPaths.push(file.torrentUrl) | ||
140 | previousPaths.push(file.metadataUrl) | ||
141 | |||
142 | const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl }) | ||
143 | previousPaths.push(JSON.stringify(metadata)) | ||
144 | } | ||
145 | |||
146 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
147 | for (const s of storyboards) { | ||
148 | previousPaths.push(s.storyboardPath) | ||
149 | } | ||
150 | } | ||
151 | |||
152 | replaceDate = new Date() | ||
153 | |||
154 | await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' }) | ||
155 | await waitJobs(servers) | ||
156 | |||
157 | for (const server of servers) { | ||
158 | const video = await server.videos.get({ id: uuid }) | ||
159 | |||
160 | expect(video.inputFileUpdatedAt).to.not.be.null | ||
161 | expect(new Date(video.inputFileUpdatedAt)).to.be.above(replaceDate) | ||
162 | |||
163 | const files = getAllFiles(video) | ||
164 | expect(files).to.have.lengthOf(4 * 2) | ||
165 | |||
166 | expect(previousPaths).to.not.include(video.previewPath) | ||
167 | expect(previousPaths).to.not.include(video.thumbnailPath) | ||
168 | |||
169 | await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
170 | await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
171 | |||
172 | for (const file of files) { | ||
173 | expect(previousPaths).to.not.include(file.fileUrl) | ||
174 | expect(previousPaths).to.not.include(file.torrentUrl) | ||
175 | expect(previousPaths).to.not.include(file.metadataUrl) | ||
176 | |||
177 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
178 | await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
179 | |||
180 | const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl }) | ||
181 | expect(previousPaths).to.not.include(JSON.stringify(metadata)) | ||
182 | } | ||
183 | |||
184 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
185 | for (const s of storyboards) { | ||
186 | expect(previousPaths).to.not.include(s.storyboardPath) | ||
187 | |||
188 | await makeGetRequest({ url: server.url, path: s.storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
189 | } | ||
190 | } | ||
191 | |||
192 | await servers[0].config.enableMinimumTranscoding() | ||
193 | }) | ||
194 | |||
195 | it('Should have cleaned up old files', async function () { | ||
196 | { | ||
197 | const count = await servers[0].servers.countFiles('storyboards') | ||
198 | expect(count).to.equal(2) | ||
199 | } | ||
200 | |||
201 | { | ||
202 | const count = await servers[0].servers.countFiles('web-videos') | ||
203 | expect(count).to.equal(5 + 1) // +1 for private directory | ||
204 | } | ||
205 | |||
206 | { | ||
207 | const count = await servers[0].servers.countFiles('streaming-playlists/hls') | ||
208 | expect(count).to.equal(1 + 1) // +1 for private directory | ||
209 | } | ||
210 | |||
211 | { | ||
212 | const count = await servers[0].servers.countFiles('torrents') | ||
213 | expect(count).to.equal(9) | ||
214 | } | ||
215 | }) | ||
216 | |||
217 | it('Should have the correct source input', async function () { | ||
218 | const source = await servers[0].videos.getSource({ id: uuid }) | ||
219 | |||
220 | expect(source.filename).to.equal('video_short_360p.mp4') | ||
221 | expect(new Date(source.createdAt)).to.be.above(replaceDate) | ||
222 | }) | ||
223 | |||
224 | it('Should not have regenerated miniatures that were previously uploaded', async function () { | ||
225 | this.timeout(120000) | ||
226 | |||
227 | const { uuid } = await servers[0].videos.upload({ | ||
228 | attributes: { | ||
229 | name: 'custom miniatures', | ||
230 | thumbnailfile: 'custom-thumbnail.jpg', | ||
231 | previewfile: 'custom-preview.jpg' | ||
232 | } | ||
233 | }) | ||
234 | |||
235 | await waitJobs(servers) | ||
236 | |||
237 | const previousPaths: string[] = [] | ||
238 | |||
239 | for (const server of servers) { | ||
240 | const video = await server.videos.get({ id: uuid }) | ||
241 | |||
242 | previousPaths.push(video.previewPath) | ||
243 | previousPaths.push(video.thumbnailPath) | ||
244 | |||
245 | await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
246 | await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
247 | } | ||
248 | |||
249 | await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' }) | ||
250 | await waitJobs(servers) | ||
251 | |||
252 | for (const server of servers) { | ||
253 | const video = await server.videos.get({ id: uuid }) | ||
254 | |||
255 | expect(previousPaths).to.include(video.previewPath) | ||
256 | expect(previousPaths).to.include(video.thumbnailPath) | ||
257 | |||
258 | await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
259 | await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
260 | } | ||
261 | }) | ||
262 | }) | ||
263 | |||
264 | describe('Autoblacklist', function () { | ||
265 | |||
266 | function updateAutoBlacklist (enabled: boolean) { | ||
267 | return servers[0].config.updateExistingSubConfig({ | ||
268 | newConfig: { | ||
269 | autoBlacklist: { | ||
270 | videos: { | ||
271 | ofUsers: { | ||
272 | enabled | ||
273 | } | ||
274 | } | ||
275 | } | ||
276 | } | ||
277 | }) | ||
278 | } | ||
279 | |||
280 | async function expectBlacklist (uuid: string, value: boolean) { | ||
281 | const video = await servers[0].videos.getWithToken({ id: uuid }) | ||
282 | |||
283 | expect(video.blacklisted).to.equal(value) | ||
284 | } | ||
285 | |||
286 | before(async function () { | ||
287 | await updateAutoBlacklist(true) | ||
288 | }) | ||
289 | |||
290 | it('Should auto blacklist an unblacklisted video after file replacement', async function () { | ||
291 | this.timeout(120000) | ||
292 | |||
293 | const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' }) | ||
294 | await waitJobs(servers) | ||
295 | await expectBlacklist(uuid, true) | ||
296 | |||
297 | await servers[0].blacklist.remove({ videoId: uuid }) | ||
298 | await expectBlacklist(uuid, false) | ||
299 | |||
300 | await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short_360p.mp4' }) | ||
301 | await waitJobs(servers) | ||
302 | |||
303 | await expectBlacklist(uuid, true) | ||
304 | }) | ||
305 | |||
306 | it('Should auto blacklist an already blacklisted video after file replacement', async function () { | ||
307 | this.timeout(120000) | ||
308 | |||
309 | const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' }) | ||
310 | await waitJobs(servers) | ||
311 | await expectBlacklist(uuid, true) | ||
312 | |||
313 | await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short_360p.mp4' }) | ||
314 | await waitJobs(servers) | ||
315 | |||
316 | await expectBlacklist(uuid, true) | ||
317 | }) | ||
318 | |||
319 | it('Should not auto blacklist if auto blacklist has been disabled between the upload and the replacement', async function () { | ||
320 | this.timeout(120000) | ||
321 | |||
322 | const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' }) | ||
323 | await waitJobs(servers) | ||
324 | await expectBlacklist(uuid, true) | ||
325 | |||
326 | await servers[0].blacklist.remove({ videoId: uuid }) | ||
327 | await expectBlacklist(uuid, false) | ||
328 | |||
329 | await updateAutoBlacklist(false) | ||
330 | |||
331 | await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short1.webm' }) | ||
332 | await waitJobs(servers) | ||
333 | |||
334 | await expectBlacklist(uuid, false) | ||
335 | }) | ||
336 | }) | ||
337 | |||
338 | describe('With object storage enabled', function () { | ||
339 | if (areMockObjectStorageTestsDisabled()) return | ||
340 | |||
341 | const objectStorage = new ObjectStorageCommand() | ||
342 | |||
343 | before(async function () { | ||
344 | this.timeout(120000) | ||
345 | |||
346 | const configOverride = objectStorage.getDefaultMockConfig() | ||
347 | await objectStorage.prepareDefaultMockBuckets() | ||
348 | |||
349 | await servers[0].kill() | ||
350 | await servers[0].run(configOverride) | ||
351 | }) | ||
352 | |||
353 | it('Should replace a video file with transcoding disabled', async function () { | ||
354 | this.timeout(120000) | ||
355 | |||
356 | await servers[0].config.disableTranscoding() | ||
357 | |||
358 | const { uuid } = await servers[0].videos.quickUpload({ | ||
359 | name: 'object storage without transcoding', | ||
360 | fixture: 'video_short_720p.mp4' | ||
361 | }) | ||
362 | await waitJobs(servers) | ||
363 | |||
364 | for (const server of servers) { | ||
365 | const video = await server.videos.get({ id: uuid }) | ||
366 | |||
367 | const files = getAllFiles(video) | ||
368 | expect(files).to.have.lengthOf(1) | ||
369 | expect(files[0].resolution.id).to.equal(720) | ||
370 | expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
371 | } | ||
372 | |||
373 | await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' }) | ||
374 | await waitJobs(servers) | ||
375 | |||
376 | for (const server of servers) { | ||
377 | const video = await server.videos.get({ id: uuid }) | ||
378 | |||
379 | const files = getAllFiles(video) | ||
380 | expect(files).to.have.lengthOf(1) | ||
381 | expect(files[0].resolution.id).to.equal(360) | ||
382 | expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
383 | } | ||
384 | }) | ||
385 | |||
386 | it('Should replace a video file with transcoding enabled', async function () { | ||
387 | this.timeout(120000) | ||
388 | |||
389 | const previousPaths: string[] = [] | ||
390 | |||
391 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true }) | ||
392 | |||
393 | const { uuid: videoUUID } = await servers[0].videos.quickUpload({ | ||
394 | name: 'object storage with transcoding', | ||
395 | fixture: 'video_short_360p.mp4' | ||
396 | }) | ||
397 | uuid = videoUUID | ||
398 | |||
399 | await waitJobs(servers) | ||
400 | |||
401 | for (const server of servers) { | ||
402 | const video = await server.videos.get({ id: uuid }) | ||
403 | |||
404 | const files = getAllFiles(video) | ||
405 | expect(files).to.have.lengthOf(4 * 2) | ||
406 | |||
407 | for (const file of files) { | ||
408 | previousPaths.push(file.fileUrl) | ||
409 | } | ||
410 | |||
411 | for (const file of video.files) { | ||
412 | expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
413 | } | ||
414 | |||
415 | for (const file of video.streamingPlaylists[0].files) { | ||
416 | expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
417 | } | ||
418 | } | ||
419 | |||
420 | await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_240p.mp4' }) | ||
421 | await waitJobs(servers) | ||
422 | |||
423 | for (const server of servers) { | ||
424 | const video = await server.videos.get({ id: uuid }) | ||
425 | |||
426 | const files = getAllFiles(video) | ||
427 | expect(files).to.have.lengthOf(3 * 2) | ||
428 | |||
429 | for (const file of files) { | ||
430 | expect(previousPaths).to.not.include(file.fileUrl) | ||
431 | } | ||
432 | |||
433 | for (const file of video.files) { | ||
434 | expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
435 | } | ||
436 | |||
437 | for (const file of video.streamingPlaylists[0].files) { | ||
438 | expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
439 | } | ||
440 | } | ||
441 | }) | ||
442 | }) | ||
443 | }) | ||
444 | |||
445 | after(async function () { | ||
446 | await cleanupTests(servers) | ||
447 | }) | ||
448 | }) | ||
diff --git a/packages/tests/src/api/videos/video-static-file-privacy.ts b/packages/tests/src/api/videos/video-static-file-privacy.ts new file mode 100644 index 000000000..7c8d14815 --- /dev/null +++ b/packages/tests/src/api/videos/video-static-file-privacy.ts | |||
@@ -0,0 +1,602 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { decode } from 'magnet-uri' | ||
5 | import { getAllFiles, wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | findExternalSavedVideo, | ||
11 | makeRawRequest, | ||
12 | PeerTubeServer, | ||
13 | sendRTMPStream, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | stopFfmpeg, | ||
17 | waitJobs | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | import { expectStartWith } from '@tests/shared/checks.js' | ||
20 | import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js' | ||
21 | import { parseTorrentVideo } from '@tests/shared/webtorrent.js' | ||
22 | |||
23 | describe('Test video static file privacy', function () { | ||
24 | let server: PeerTubeServer | ||
25 | let userToken: string | ||
26 | |||
27 | before(async function () { | ||
28 | this.timeout(50000) | ||
29 | |||
30 | server = await createSingleServer(1) | ||
31 | await setAccessTokensToServers([ server ]) | ||
32 | await setDefaultVideoChannel([ server ]) | ||
33 | |||
34 | userToken = await server.users.generateUserAndToken('user1') | ||
35 | }) | ||
36 | |||
37 | describe('VOD static file path', function () { | ||
38 | |||
39 | function runSuite () { | ||
40 | |||
41 | async function checkPrivateFiles (uuid: string) { | ||
42 | const video = await server.videos.getWithToken({ id: uuid }) | ||
43 | |||
44 | for (const file of video.files) { | ||
45 | expect(file.fileDownloadUrl).to.not.include('/private/') | ||
46 | expectStartWith(file.fileUrl, server.url + '/static/web-videos/private/') | ||
47 | |||
48 | const torrent = await parseTorrentVideo(server, file) | ||
49 | expect(torrent.urlList).to.have.lengthOf(0) | ||
50 | |||
51 | const magnet = decode(file.magnetUri) | ||
52 | expect(magnet.urlList).to.have.lengthOf(0) | ||
53 | |||
54 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
55 | } | ||
56 | |||
57 | const hls = video.streamingPlaylists[0] | ||
58 | if (hls) { | ||
59 | expectStartWith(hls.playlistUrl, server.url + '/static/streaming-playlists/hls/private/') | ||
60 | expectStartWith(hls.segmentsSha256Url, server.url + '/static/streaming-playlists/hls/private/') | ||
61 | |||
62 | await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
63 | await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
64 | } | ||
65 | } | ||
66 | |||
67 | async function checkPublicFiles (uuid: string) { | ||
68 | const video = await server.videos.get({ id: uuid }) | ||
69 | |||
70 | for (const file of getAllFiles(video)) { | ||
71 | expect(file.fileDownloadUrl).to.not.include('/private/') | ||
72 | expect(file.fileUrl).to.not.include('/private/') | ||
73 | |||
74 | const torrent = await parseTorrentVideo(server, file) | ||
75 | expect(torrent.urlList[0]).to.not.include('private') | ||
76 | |||
77 | const magnet = decode(file.magnetUri) | ||
78 | expect(magnet.urlList[0]).to.not.include('private') | ||
79 | |||
80 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
81 | await makeRawRequest({ url: torrent.urlList[0], expectedStatus: HttpStatusCode.OK_200 }) | ||
82 | await makeRawRequest({ url: magnet.urlList[0], expectedStatus: HttpStatusCode.OK_200 }) | ||
83 | } | ||
84 | |||
85 | const hls = video.streamingPlaylists[0] | ||
86 | if (hls) { | ||
87 | expect(hls.playlistUrl).to.not.include('private') | ||
88 | expect(hls.segmentsSha256Url).to.not.include('private') | ||
89 | |||
90 | await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
91 | await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
92 | } | ||
93 | } | ||
94 | |||
95 | it('Should upload a private/internal/password protected video and have a private static path', async function () { | ||
96 | this.timeout(120000) | ||
97 | |||
98 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | ||
99 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy }) | ||
100 | await waitJobs([ server ]) | ||
101 | |||
102 | await checkPrivateFiles(uuid) | ||
103 | } | ||
104 | |||
105 | const { uuid } = await server.videos.quickUpload({ | ||
106 | name: 'video', | ||
107 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
108 | videoPasswords: [ 'my super password' ] | ||
109 | }) | ||
110 | await waitJobs([ server ]) | ||
111 | |||
112 | await checkPrivateFiles(uuid) | ||
113 | }) | ||
114 | |||
115 | it('Should upload a public video and update it as private/internal to have a private static path', async function () { | ||
116 | this.timeout(120000) | ||
117 | |||
118 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | ||
119 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PUBLIC }) | ||
120 | await waitJobs([ server ]) | ||
121 | |||
122 | await server.videos.update({ id: uuid, attributes: { privacy } }) | ||
123 | await waitJobs([ server ]) | ||
124 | |||
125 | await checkPrivateFiles(uuid) | ||
126 | } | ||
127 | }) | ||
128 | |||
129 | it('Should upload a private video and update it to unlisted to have a public static path', async function () { | ||
130 | this.timeout(120000) | ||
131 | |||
132 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
133 | await waitJobs([ server ]) | ||
134 | |||
135 | await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } }) | ||
136 | await waitJobs([ server ]) | ||
137 | |||
138 | await checkPublicFiles(uuid) | ||
139 | }) | ||
140 | |||
141 | it('Should upload an internal video and update it to public to have a public static path', async function () { | ||
142 | this.timeout(120000) | ||
143 | |||
144 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) | ||
145 | await waitJobs([ server ]) | ||
146 | |||
147 | await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } }) | ||
148 | await waitJobs([ server ]) | ||
149 | |||
150 | await checkPublicFiles(uuid) | ||
151 | }) | ||
152 | |||
153 | it('Should upload an internal video and schedule a public publish', async function () { | ||
154 | this.timeout(120000) | ||
155 | |||
156 | const attributes = { | ||
157 | name: 'video', | ||
158 | privacy: VideoPrivacy.PRIVATE, | ||
159 | scheduleUpdate: { | ||
160 | updateAt: new Date(Date.now() + 1000).toISOString(), | ||
161 | privacy: VideoPrivacy.PUBLIC | ||
162 | } | ||
163 | } | ||
164 | |||
165 | const { uuid } = await server.videos.upload({ attributes }) | ||
166 | |||
167 | await waitJobs([ server ]) | ||
168 | await wait(1000) | ||
169 | await server.debug.sendCommand({ body: { command: 'process-update-videos-scheduler' } }) | ||
170 | |||
171 | await waitJobs([ server ]) | ||
172 | |||
173 | await checkPublicFiles(uuid) | ||
174 | }) | ||
175 | } | ||
176 | |||
177 | describe('Without transcoding', function () { | ||
178 | runSuite() | ||
179 | }) | ||
180 | |||
181 | describe('With transcoding', function () { | ||
182 | |||
183 | before(async function () { | ||
184 | await server.config.enableMinimumTranscoding() | ||
185 | }) | ||
186 | |||
187 | runSuite() | ||
188 | }) | ||
189 | }) | ||
190 | |||
191 | describe('VOD static file right check', function () { | ||
192 | let unrelatedFileToken: string | ||
193 | |||
194 | async function checkVideoFiles (options: { | ||
195 | id: string | ||
196 | expectedStatus: HttpStatusCodeType | ||
197 | token: string | ||
198 | videoFileToken: string | ||
199 | videoPassword?: string | ||
200 | }) { | ||
201 | const { id, expectedStatus, token, videoFileToken, videoPassword } = options | ||
202 | |||
203 | const video = await server.videos.getWithToken({ id }) | ||
204 | |||
205 | for (const file of getAllFiles(video)) { | ||
206 | await makeRawRequest({ url: file.fileUrl, token, expectedStatus }) | ||
207 | await makeRawRequest({ url: file.fileDownloadUrl, token, expectedStatus }) | ||
208 | |||
209 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) | ||
210 | await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) | ||
211 | |||
212 | if (videoPassword) { | ||
213 | const headers = { 'x-peertube-video-password': videoPassword } | ||
214 | await makeRawRequest({ url: file.fileUrl, headers, expectedStatus }) | ||
215 | await makeRawRequest({ url: file.fileDownloadUrl, headers, expectedStatus }) | ||
216 | } | ||
217 | } | ||
218 | |||
219 | const hls = video.streamingPlaylists[0] | ||
220 | await makeRawRequest({ url: hls.playlistUrl, token, expectedStatus }) | ||
221 | await makeRawRequest({ url: hls.segmentsSha256Url, token, expectedStatus }) | ||
222 | |||
223 | await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) | ||
224 | await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) | ||
225 | |||
226 | if (videoPassword) { | ||
227 | const headers = { 'x-peertube-video-password': videoPassword } | ||
228 | await makeRawRequest({ url: hls.playlistUrl, token: null, headers, expectedStatus }) | ||
229 | await makeRawRequest({ url: hls.segmentsSha256Url, token: null, headers, expectedStatus }) | ||
230 | } | ||
231 | } | ||
232 | |||
233 | before(async function () { | ||
234 | await server.config.enableMinimumTranscoding() | ||
235 | |||
236 | const { uuid } = await server.videos.quickUpload({ name: 'another video' }) | ||
237 | unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
238 | }) | ||
239 | |||
240 | it('Should not be able to access a private video files without OAuth token and file token', async function () { | ||
241 | this.timeout(120000) | ||
242 | |||
243 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
244 | await waitJobs([ server ]) | ||
245 | |||
246 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) | ||
247 | }) | ||
248 | |||
249 | it('Should not be able to access password protected video files without OAuth token, file token and password', async function () { | ||
250 | this.timeout(120000) | ||
251 | const videoPassword = 'my super password' | ||
252 | |||
253 | const { uuid } = await server.videos.quickUpload({ | ||
254 | name: 'password protected video', | ||
255 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
256 | videoPasswords: [ videoPassword ] | ||
257 | }) | ||
258 | await waitJobs([ server ]) | ||
259 | |||
260 | await checkVideoFiles({ | ||
261 | id: uuid, | ||
262 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
263 | token: null, | ||
264 | videoFileToken: null, | ||
265 | videoPassword: null | ||
266 | }) | ||
267 | }) | ||
268 | |||
269 | it('Should not be able to access an password video files with incorrect OAuth token, file token and password', async function () { | ||
270 | this.timeout(120000) | ||
271 | const videoPassword = 'my super password' | ||
272 | |||
273 | const { uuid } = await server.videos.quickUpload({ | ||
274 | name: 'password protected video', | ||
275 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
276 | videoPasswords: [ videoPassword ] | ||
277 | }) | ||
278 | await waitJobs([ server ]) | ||
279 | |||
280 | await checkVideoFiles({ | ||
281 | id: uuid, | ||
282 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
283 | token: userToken, | ||
284 | videoFileToken: unrelatedFileToken, | ||
285 | videoPassword: 'incorrectPassword' | ||
286 | }) | ||
287 | }) | ||
288 | |||
289 | it('Should not be able to access an private video files without appropriate OAuth token and file token', async function () { | ||
290 | this.timeout(120000) | ||
291 | |||
292 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
293 | await waitJobs([ server ]) | ||
294 | |||
295 | await checkVideoFiles({ | ||
296 | id: uuid, | ||
297 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
298 | token: userToken, | ||
299 | videoFileToken: unrelatedFileToken | ||
300 | }) | ||
301 | }) | ||
302 | |||
303 | it('Should be able to access a private video files with appropriate OAuth token or file token', async function () { | ||
304 | this.timeout(120000) | ||
305 | |||
306 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
307 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
308 | |||
309 | await waitJobs([ server ]) | ||
310 | |||
311 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) | ||
312 | }) | ||
313 | |||
314 | it('Should be able to access a password protected video files with appropriate OAuth token or file token', async function () { | ||
315 | this.timeout(120000) | ||
316 | const videoPassword = 'my super password' | ||
317 | |||
318 | const { uuid } = await server.videos.quickUpload({ | ||
319 | name: 'video', | ||
320 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
321 | videoPasswords: [ videoPassword ] | ||
322 | }) | ||
323 | |||
324 | const videoFileToken = await server.videoToken.getVideoFileToken({ token: null, videoId: uuid, videoPassword }) | ||
325 | |||
326 | await waitJobs([ server ]) | ||
327 | |||
328 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken, videoPassword }) | ||
329 | }) | ||
330 | |||
331 | it('Should reinject video file token', async function () { | ||
332 | this.timeout(120000) | ||
333 | |||
334 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
335 | |||
336 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
337 | await waitJobs([ server ]) | ||
338 | |||
339 | { | ||
340 | const video = await server.videos.getWithToken({ id: uuid }) | ||
341 | const hls = video.streamingPlaylists[0] | ||
342 | const query = { videoFileToken } | ||
343 | const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
344 | |||
345 | expect(text).to.not.include(videoFileToken) | ||
346 | } | ||
347 | |||
348 | { | ||
349 | await checkVideoFileTokenReinjection({ | ||
350 | server, | ||
351 | videoUUID: uuid, | ||
352 | videoFileToken, | ||
353 | resolutions: [ 240, 720 ], | ||
354 | isLive: false | ||
355 | }) | ||
356 | } | ||
357 | }) | ||
358 | |||
359 | it('Should be able to access a private video of another user with an admin OAuth token or file token', async function () { | ||
360 | this.timeout(120000) | ||
361 | |||
362 | const { uuid } = await server.videos.quickUpload({ name: 'video', token: userToken, privacy: VideoPrivacy.PRIVATE }) | ||
363 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
364 | |||
365 | await waitJobs([ server ]) | ||
366 | |||
367 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) | ||
368 | }) | ||
369 | }) | ||
370 | |||
371 | describe('Live static file path and check', function () { | ||
372 | let normalLiveId: string | ||
373 | let normalLive: LiveVideo | ||
374 | |||
375 | let permanentLiveId: string | ||
376 | let permanentLive: LiveVideo | ||
377 | |||
378 | let passwordProtectedLiveId: string | ||
379 | let passwordProtectedLive: LiveVideo | ||
380 | |||
381 | const correctPassword = 'my super password' | ||
382 | |||
383 | let unrelatedFileToken: string | ||
384 | |||
385 | async function checkLiveFiles (options: { live: LiveVideo, liveId: string, videoPassword?: string }) { | ||
386 | const { live, liveId, videoPassword } = options | ||
387 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
388 | await server.live.waitUntilPublished({ videoId: liveId }) | ||
389 | |||
390 | const video = await server.videos.getWithToken({ id: liveId }) | ||
391 | |||
392 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
393 | |||
394 | const hls = video.streamingPlaylists[0] | ||
395 | |||
396 | for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { | ||
397 | expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/') | ||
398 | |||
399 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
400 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
401 | |||
402 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
403 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
404 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
405 | |||
406 | if (videoPassword) { | ||
407 | await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 }) | ||
408 | await makeRawRequest({ | ||
409 | url, | ||
410 | headers: { 'x-peertube-video-password': 'incorrectPassword' }, | ||
411 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
412 | }) | ||
413 | } | ||
414 | |||
415 | } | ||
416 | |||
417 | await stopFfmpeg(ffmpegCommand) | ||
418 | } | ||
419 | |||
420 | async function checkReplay (replay: VideoDetails) { | ||
421 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: replay.uuid }) | ||
422 | |||
423 | const hls = replay.streamingPlaylists[0] | ||
424 | expect(hls.files).to.not.have.lengthOf(0) | ||
425 | |||
426 | for (const file of hls.files) { | ||
427 | await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
428 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
429 | |||
430 | await makeRawRequest({ url: file.fileUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
431 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
432 | await makeRawRequest({ | ||
433 | url: file.fileUrl, | ||
434 | query: { videoFileToken: unrelatedFileToken }, | ||
435 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
436 | }) | ||
437 | } | ||
438 | |||
439 | for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) { | ||
440 | expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/') | ||
441 | |||
442 | await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
443 | await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 }) | ||
444 | |||
445 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
446 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
447 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
448 | } | ||
449 | } | ||
450 | |||
451 | before(async function () { | ||
452 | await server.config.enableMinimumTranscoding() | ||
453 | |||
454 | const { uuid } = await server.videos.quickUpload({ name: 'another video' }) | ||
455 | unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
456 | |||
457 | await server.config.enableLive({ | ||
458 | allowReplay: true, | ||
459 | transcoding: true, | ||
460 | resolutions: 'min' | ||
461 | }) | ||
462 | |||
463 | { | ||
464 | const { video, live } = await server.live.quickCreate({ | ||
465 | saveReplay: true, | ||
466 | permanentLive: false, | ||
467 | privacy: VideoPrivacy.PRIVATE | ||
468 | }) | ||
469 | normalLiveId = video.uuid | ||
470 | normalLive = live | ||
471 | } | ||
472 | |||
473 | { | ||
474 | const { video, live } = await server.live.quickCreate({ | ||
475 | saveReplay: true, | ||
476 | permanentLive: true, | ||
477 | privacy: VideoPrivacy.PRIVATE | ||
478 | }) | ||
479 | permanentLiveId = video.uuid | ||
480 | permanentLive = live | ||
481 | } | ||
482 | |||
483 | { | ||
484 | const { video, live } = await server.live.quickCreate({ | ||
485 | saveReplay: false, | ||
486 | permanentLive: false, | ||
487 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
488 | videoPasswords: [ correctPassword ] | ||
489 | }) | ||
490 | passwordProtectedLiveId = video.uuid | ||
491 | passwordProtectedLive = live | ||
492 | } | ||
493 | }) | ||
494 | |||
495 | it('Should create a private normal live and have a private static path', async function () { | ||
496 | this.timeout(240000) | ||
497 | |||
498 | await checkLiveFiles({ live: normalLive, liveId: normalLiveId }) | ||
499 | }) | ||
500 | |||
501 | it('Should create a private permanent live and have a private static path', async function () { | ||
502 | this.timeout(240000) | ||
503 | |||
504 | await checkLiveFiles({ live: permanentLive, liveId: permanentLiveId }) | ||
505 | }) | ||
506 | |||
507 | it('Should create a password protected live and have a private static path', async function () { | ||
508 | this.timeout(240000) | ||
509 | |||
510 | await checkLiveFiles({ live: passwordProtectedLive, liveId: passwordProtectedLiveId, videoPassword: correctPassword }) | ||
511 | }) | ||
512 | |||
513 | it('Should reinject video file token on permanent live', async function () { | ||
514 | this.timeout(240000) | ||
515 | |||
516 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey }) | ||
517 | await server.live.waitUntilPublished({ videoId: permanentLiveId }) | ||
518 | |||
519 | const video = await server.videos.getWithToken({ id: permanentLiveId }) | ||
520 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
521 | const hls = video.streamingPlaylists[0] | ||
522 | |||
523 | { | ||
524 | const query = { videoFileToken } | ||
525 | const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
526 | |||
527 | expect(text).to.not.include(videoFileToken) | ||
528 | } | ||
529 | |||
530 | { | ||
531 | await checkVideoFileTokenReinjection({ | ||
532 | server, | ||
533 | videoUUID: permanentLiveId, | ||
534 | videoFileToken, | ||
535 | resolutions: [ 720 ], | ||
536 | isLive: true | ||
537 | }) | ||
538 | } | ||
539 | |||
540 | await stopFfmpeg(ffmpegCommand) | ||
541 | }) | ||
542 | |||
543 | it('Should have created a replay of the normal live with a private static path', async function () { | ||
544 | this.timeout(240000) | ||
545 | |||
546 | await server.live.waitUntilReplacedByReplay({ videoId: normalLiveId }) | ||
547 | |||
548 | const replay = await server.videos.getWithToken({ id: normalLiveId }) | ||
549 | await checkReplay(replay) | ||
550 | }) | ||
551 | |||
552 | it('Should have created a replay of the permanent live with a private static path', async function () { | ||
553 | this.timeout(240000) | ||
554 | |||
555 | await server.live.waitUntilWaiting({ videoId: permanentLiveId }) | ||
556 | await waitJobs([ server ]) | ||
557 | |||
558 | const live = await server.videos.getWithToken({ id: permanentLiveId }) | ||
559 | const replayFromList = await findExternalSavedVideo(server, live) | ||
560 | const replay = await server.videos.getWithToken({ id: replayFromList.id }) | ||
561 | |||
562 | await checkReplay(replay) | ||
563 | }) | ||
564 | }) | ||
565 | |||
566 | describe('With static file right check disabled', function () { | ||
567 | let videoUUID: string | ||
568 | |||
569 | before(async function () { | ||
570 | this.timeout(240000) | ||
571 | |||
572 | await server.kill() | ||
573 | |||
574 | await server.run({ | ||
575 | static_files: { | ||
576 | private_files_require_auth: false | ||
577 | } | ||
578 | }) | ||
579 | |||
580 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) | ||
581 | videoUUID = uuid | ||
582 | |||
583 | await waitJobs([ server ]) | ||
584 | }) | ||
585 | |||
586 | it('Should not check auth for private static files', async function () { | ||
587 | const video = await server.videos.getWithToken({ id: videoUUID }) | ||
588 | |||
589 | for (const file of getAllFiles(video)) { | ||
590 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
591 | } | ||
592 | |||
593 | const hls = video.streamingPlaylists[0] | ||
594 | await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
595 | await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
596 | }) | ||
597 | }) | ||
598 | |||
599 | after(async function () { | ||
600 | await cleanupTests([ server ]) | ||
601 | }) | ||
602 | }) | ||
diff --git a/packages/tests/src/api/videos/video-storyboard.ts b/packages/tests/src/api/videos/video-storyboard.ts new file mode 100644 index 000000000..7d156aa7f --- /dev/null +++ b/packages/tests/src/api/videos/video-storyboard.ts | |||
@@ -0,0 +1,213 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { readdir } from 'fs/promises' | ||
5 | import { basename } from 'path' | ||
6 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
7 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
8 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
9 | import { | ||
10 | cleanupTests, | ||
11 | createMultipleServers, | ||
12 | doubleFollow, | ||
13 | makeGetRequest, | ||
14 | PeerTubeServer, | ||
15 | sendRTMPStream, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel, | ||
18 | stopFfmpeg, | ||
19 | waitJobs | ||
20 | } from '@peertube/peertube-server-commands' | ||
21 | |||
22 | async function checkStoryboard (options: { | ||
23 | server: PeerTubeServer | ||
24 | uuid: string | ||
25 | tilesCount?: number | ||
26 | minSize?: number | ||
27 | }) { | ||
28 | const { server, uuid, tilesCount, minSize = 1000 } = options | ||
29 | |||
30 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
31 | |||
32 | expect(storyboards).to.have.lengthOf(1) | ||
33 | |||
34 | const storyboard = storyboards[0] | ||
35 | |||
36 | expect(storyboard.spriteDuration).to.equal(1) | ||
37 | expect(storyboard.spriteHeight).to.equal(108) | ||
38 | expect(storyboard.spriteWidth).to.equal(192) | ||
39 | expect(storyboard.storyboardPath).to.exist | ||
40 | |||
41 | if (tilesCount) { | ||
42 | expect(storyboard.totalWidth).to.equal(192 * Math.min(tilesCount, 10)) | ||
43 | expect(storyboard.totalHeight).to.equal(108 * Math.max((tilesCount / 10), 1)) | ||
44 | } | ||
45 | |||
46 | const { body } = await makeGetRequest({ url: server.url, path: storyboard.storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
47 | expect(body.length).to.be.above(minSize) | ||
48 | } | ||
49 | |||
50 | describe('Test video storyboard', function () { | ||
51 | let servers: PeerTubeServer[] | ||
52 | |||
53 | let baseUUID: string | ||
54 | |||
55 | before(async function () { | ||
56 | this.timeout(120000) | ||
57 | |||
58 | servers = await createMultipleServers(2) | ||
59 | await setAccessTokensToServers(servers) | ||
60 | await setDefaultVideoChannel(servers) | ||
61 | |||
62 | await doubleFollow(servers[0], servers[1]) | ||
63 | }) | ||
64 | |||
65 | it('Should generate a storyboard after upload without transcoding', async function () { | ||
66 | this.timeout(120000) | ||
67 | |||
68 | // 5s video | ||
69 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' }) | ||
70 | baseUUID = uuid | ||
71 | await waitJobs(servers) | ||
72 | |||
73 | for (const server of servers) { | ||
74 | await checkStoryboard({ server, uuid, tilesCount: 5 }) | ||
75 | } | ||
76 | }) | ||
77 | |||
78 | it('Should generate a storyboard after upload without transcoding with a long video', async function () { | ||
79 | this.timeout(120000) | ||
80 | |||
81 | // 124s video | ||
82 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_very_long_10p.mp4' }) | ||
83 | await waitJobs(servers) | ||
84 | |||
85 | for (const server of servers) { | ||
86 | await checkStoryboard({ server, uuid, tilesCount: 100 }) | ||
87 | } | ||
88 | }) | ||
89 | |||
90 | it('Should generate a storyboard after upload with transcoding', async function () { | ||
91 | this.timeout(120000) | ||
92 | |||
93 | await servers[0].config.enableMinimumTranscoding() | ||
94 | |||
95 | // 5s video | ||
96 | const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' }) | ||
97 | await waitJobs(servers) | ||
98 | |||
99 | for (const server of servers) { | ||
100 | await checkStoryboard({ server, uuid, tilesCount: 5 }) | ||
101 | } | ||
102 | }) | ||
103 | |||
104 | it('Should generate a storyboard after an audio upload', async function () { | ||
105 | this.timeout(120000) | ||
106 | |||
107 | // 6s audio | ||
108 | const attributes = { name: 'audio', fixture: 'sample.ogg' } | ||
109 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' }) | ||
110 | await waitJobs(servers) | ||
111 | |||
112 | for (const server of servers) { | ||
113 | try { | ||
114 | await checkStoryboard({ server, uuid, tilesCount: 6, minSize: 250 }) | ||
115 | } catch { // FIXME: to remove after ffmpeg CI upgrade, ffmpeg CI version (4.3) generates a 7.6s length video | ||
116 | await checkStoryboard({ server, uuid, tilesCount: 8, minSize: 250 }) | ||
117 | } | ||
118 | } | ||
119 | }) | ||
120 | |||
121 | it('Should generate a storyboard after HTTP import', async function () { | ||
122 | this.timeout(120000) | ||
123 | |||
124 | if (areHttpImportTestsDisabled()) return | ||
125 | |||
126 | // 3s video | ||
127 | const { video } = await servers[0].imports.importVideo({ | ||
128 | attributes: { | ||
129 | targetUrl: FIXTURE_URLS.goodVideo, | ||
130 | channelId: servers[0].store.channel.id, | ||
131 | privacy: VideoPrivacy.PUBLIC | ||
132 | } | ||
133 | }) | ||
134 | await waitJobs(servers) | ||
135 | |||
136 | for (const server of servers) { | ||
137 | await checkStoryboard({ server, uuid: video.uuid, tilesCount: 3 }) | ||
138 | } | ||
139 | }) | ||
140 | |||
141 | it('Should generate a storyboard after torrent import', async function () { | ||
142 | this.timeout(120000) | ||
143 | |||
144 | if (areHttpImportTestsDisabled()) return | ||
145 | |||
146 | // 10s video | ||
147 | const { video } = await servers[0].imports.importVideo({ | ||
148 | attributes: { | ||
149 | magnetUri: FIXTURE_URLS.magnet, | ||
150 | channelId: servers[0].store.channel.id, | ||
151 | privacy: VideoPrivacy.PUBLIC | ||
152 | } | ||
153 | }) | ||
154 | await waitJobs(servers) | ||
155 | |||
156 | for (const server of servers) { | ||
157 | await checkStoryboard({ server, uuid: video.uuid, tilesCount: 10 }) | ||
158 | } | ||
159 | }) | ||
160 | |||
161 | it('Should generate a storyboard after a live', async function () { | ||
162 | this.timeout(240000) | ||
163 | |||
164 | await servers[0].config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) | ||
165 | |||
166 | const { live, video } = await servers[0].live.quickCreate({ | ||
167 | saveReplay: true, | ||
168 | permanentLive: false, | ||
169 | privacy: VideoPrivacy.PUBLIC | ||
170 | }) | ||
171 | |||
172 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | ||
173 | await servers[0].live.waitUntilPublished({ videoId: video.id }) | ||
174 | |||
175 | await stopFfmpeg(ffmpegCommand) | ||
176 | |||
177 | await servers[0].live.waitUntilReplacedByReplay({ videoId: video.id }) | ||
178 | await waitJobs(servers) | ||
179 | |||
180 | for (const server of servers) { | ||
181 | await checkStoryboard({ server, uuid: video.uuid }) | ||
182 | } | ||
183 | }) | ||
184 | |||
185 | it('Should cleanup storyboards on video deletion', async function () { | ||
186 | this.timeout(60000) | ||
187 | |||
188 | const { storyboards } = await servers[0].storyboard.list({ id: baseUUID }) | ||
189 | const storyboardName = basename(storyboards[0].storyboardPath) | ||
190 | |||
191 | const listFiles = () => { | ||
192 | const storyboardPath = servers[0].getDirectoryPath('storyboards') | ||
193 | return readdir(storyboardPath) | ||
194 | } | ||
195 | |||
196 | { | ||
197 | const storyboads = await listFiles() | ||
198 | expect(storyboads).to.include(storyboardName) | ||
199 | } | ||
200 | |||
201 | await servers[0].videos.remove({ id: baseUUID }) | ||
202 | await waitJobs(servers) | ||
203 | |||
204 | { | ||
205 | const storyboads = await listFiles() | ||
206 | expect(storyboads).to.not.include(storyboardName) | ||
207 | } | ||
208 | }) | ||
209 | |||
210 | after(async function () { | ||
211 | await cleanupTests(servers) | ||
212 | }) | ||
213 | }) | ||
diff --git a/packages/tests/src/api/videos/videos-common-filters.ts b/packages/tests/src/api/videos/videos-common-filters.ts new file mode 100644 index 000000000..9e75bd6ca --- /dev/null +++ b/packages/tests/src/api/videos/videos-common-filters.ts | |||
@@ -0,0 +1,499 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pick } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | HttpStatusCode, | ||
7 | HttpStatusCodeType, | ||
8 | UserRole, | ||
9 | Video, | ||
10 | VideoDetails, | ||
11 | VideoInclude, | ||
12 | VideoIncludeType, | ||
13 | VideoPrivacy, | ||
14 | VideoPrivacyType | ||
15 | } from '@peertube/peertube-models' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | makeGetRequest, | ||
21 | PeerTubeServer, | ||
22 | setAccessTokensToServers, | ||
23 | setDefaultAccountAvatar, | ||
24 | setDefaultVideoChannel, | ||
25 | waitJobs | ||
26 | } from '@peertube/peertube-server-commands' | ||
27 | |||
28 | describe('Test videos filter', function () { | ||
29 | let servers: PeerTubeServer[] | ||
30 | let paths: string[] | ||
31 | let remotePaths: string[] | ||
32 | |||
33 | const subscriptionVideosPath = '/api/v1/users/me/subscriptions/videos' | ||
34 | |||
35 | // --------------------------------------------------------------- | ||
36 | |||
37 | before(async function () { | ||
38 | this.timeout(240000) | ||
39 | |||
40 | servers = await createMultipleServers(2) | ||
41 | |||
42 | await setAccessTokensToServers(servers) | ||
43 | await setDefaultVideoChannel(servers) | ||
44 | await setDefaultAccountAvatar(servers) | ||
45 | |||
46 | await servers[1].config.enableMinimumTranscoding() | ||
47 | |||
48 | for (const server of servers) { | ||
49 | const moderator = { username: 'moderator', password: 'my super password' } | ||
50 | await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR }) | ||
51 | server['moderatorAccessToken'] = await server.login.getAccessToken(moderator) | ||
52 | |||
53 | await server.videos.upload({ attributes: { name: 'public ' + server.serverNumber } }) | ||
54 | |||
55 | { | ||
56 | const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED } | ||
57 | await server.videos.upload({ attributes }) | ||
58 | } | ||
59 | |||
60 | { | ||
61 | const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE } | ||
62 | await server.videos.upload({ attributes }) | ||
63 | } | ||
64 | |||
65 | // Subscribing to itself | ||
66 | await server.subscriptions.add({ targetUri: 'root_channel@' + server.host }) | ||
67 | } | ||
68 | |||
69 | await doubleFollow(servers[0], servers[1]) | ||
70 | |||
71 | paths = [ | ||
72 | `/api/v1/video-channels/root_channel/videos`, | ||
73 | `/api/v1/accounts/root/videos`, | ||
74 | '/api/v1/videos', | ||
75 | '/api/v1/search/videos', | ||
76 | subscriptionVideosPath | ||
77 | ] | ||
78 | |||
79 | remotePaths = [ | ||
80 | `/api/v1/video-channels/root_channel@${servers[1].host}/videos`, | ||
81 | `/api/v1/accounts/root@${servers[1].host}/videos`, | ||
82 | '/api/v1/videos', | ||
83 | '/api/v1/search/videos' | ||
84 | ] | ||
85 | }) | ||
86 | |||
87 | describe('Check videos filters', function () { | ||
88 | |||
89 | async function listVideos (options: { | ||
90 | server: PeerTubeServer | ||
91 | path: string | ||
92 | isLocal?: boolean | ||
93 | hasWebVideoFiles?: boolean | ||
94 | hasHLSFiles?: boolean | ||
95 | include?: VideoIncludeType | ||
96 | privacyOneOf?: VideoPrivacyType[] | ||
97 | category?: number | ||
98 | tagsAllOf?: string[] | ||
99 | token?: string | ||
100 | expectedStatus?: HttpStatusCodeType | ||
101 | excludeAlreadyWatched?: boolean | ||
102 | }) { | ||
103 | const res = await makeGetRequest({ | ||
104 | url: options.server.url, | ||
105 | path: options.path, | ||
106 | token: options.token ?? options.server.accessToken, | ||
107 | query: { | ||
108 | ...pick(options, [ | ||
109 | 'isLocal', | ||
110 | 'include', | ||
111 | 'category', | ||
112 | 'tagsAllOf', | ||
113 | 'hasWebVideoFiles', | ||
114 | 'hasHLSFiles', | ||
115 | 'privacyOneOf', | ||
116 | 'excludeAlreadyWatched' | ||
117 | ]), | ||
118 | |||
119 | sort: 'createdAt' | ||
120 | }, | ||
121 | expectedStatus: options.expectedStatus ?? HttpStatusCode.OK_200 | ||
122 | }) | ||
123 | |||
124 | return res.body.data as Video[] | ||
125 | } | ||
126 | |||
127 | async function getVideosNames ( | ||
128 | options: { | ||
129 | server: PeerTubeServer | ||
130 | isLocal?: boolean | ||
131 | include?: VideoIncludeType | ||
132 | privacyOneOf?: VideoPrivacyType[] | ||
133 | token?: string | ||
134 | expectedStatus?: HttpStatusCodeType | ||
135 | skipSubscription?: boolean | ||
136 | excludeAlreadyWatched?: boolean | ||
137 | } | ||
138 | ) { | ||
139 | const { skipSubscription = false } = options | ||
140 | const videosResults: string[][] = [] | ||
141 | |||
142 | for (const path of paths) { | ||
143 | if (skipSubscription && path === subscriptionVideosPath) continue | ||
144 | |||
145 | const videos = await listVideos({ ...options, path }) | ||
146 | |||
147 | videosResults.push(videos.map(v => v.name)) | ||
148 | } | ||
149 | |||
150 | return videosResults | ||
151 | } | ||
152 | |||
153 | it('Should display local videos', async function () { | ||
154 | for (const server of servers) { | ||
155 | const namesResults = await getVideosNames({ server, isLocal: true }) | ||
156 | |||
157 | for (const names of namesResults) { | ||
158 | expect(names).to.have.lengthOf(1) | ||
159 | expect(names[0]).to.equal('public ' + server.serverNumber) | ||
160 | } | ||
161 | } | ||
162 | }) | ||
163 | |||
164 | it('Should display local videos with hidden privacy by the admin or the moderator', async function () { | ||
165 | for (const server of servers) { | ||
166 | for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) { | ||
167 | |||
168 | const namesResults = await getVideosNames( | ||
169 | { | ||
170 | server, | ||
171 | token, | ||
172 | isLocal: true, | ||
173 | privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ], | ||
174 | skipSubscription: true | ||
175 | } | ||
176 | ) | ||
177 | |||
178 | for (const names of namesResults) { | ||
179 | expect(names).to.have.lengthOf(3) | ||
180 | |||
181 | expect(names[0]).to.equal('public ' + server.serverNumber) | ||
182 | expect(names[1]).to.equal('unlisted ' + server.serverNumber) | ||
183 | expect(names[2]).to.equal('private ' + server.serverNumber) | ||
184 | } | ||
185 | } | ||
186 | } | ||
187 | }) | ||
188 | |||
189 | it('Should display all videos by the admin or the moderator', async function () { | ||
190 | for (const server of servers) { | ||
191 | for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) { | ||
192 | |||
193 | const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({ | ||
194 | server, | ||
195 | token, | ||
196 | privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ] | ||
197 | }) | ||
198 | |||
199 | expect(channelVideos).to.have.lengthOf(3) | ||
200 | expect(accountVideos).to.have.lengthOf(3) | ||
201 | |||
202 | expect(videos).to.have.lengthOf(5) | ||
203 | expect(searchVideos).to.have.lengthOf(5) | ||
204 | } | ||
205 | } | ||
206 | }) | ||
207 | |||
208 | it('Should display only remote videos', async function () { | ||
209 | this.timeout(120000) | ||
210 | |||
211 | await servers[1].videos.upload({ attributes: { name: 'remote video' } }) | ||
212 | |||
213 | await waitJobs(servers) | ||
214 | |||
215 | const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video') | ||
216 | |||
217 | for (const path of remotePaths) { | ||
218 | { | ||
219 | const videos = await listVideos({ server: servers[0], path }) | ||
220 | const video = finder(videos) | ||
221 | expect(video).to.exist | ||
222 | } | ||
223 | |||
224 | { | ||
225 | const videos = await listVideos({ server: servers[0], path, isLocal: false }) | ||
226 | const video = finder(videos) | ||
227 | expect(video).to.exist | ||
228 | } | ||
229 | |||
230 | { | ||
231 | const videos = await listVideos({ server: servers[0], path, isLocal: true }) | ||
232 | const video = finder(videos) | ||
233 | expect(video).to.not.exist | ||
234 | } | ||
235 | } | ||
236 | }) | ||
237 | |||
238 | it('Should include not published videos', async function () { | ||
239 | await servers[0].config.enableLive({ allowReplay: false, transcoding: false }) | ||
240 | await servers[0].live.create({ fields: { name: 'live video', channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } }) | ||
241 | |||
242 | const finder = (videos: Video[]) => videos.find(v => v.name === 'live video') | ||
243 | |||
244 | for (const path of paths) { | ||
245 | { | ||
246 | const videos = await listVideos({ server: servers[0], path }) | ||
247 | const video = finder(videos) | ||
248 | expect(video).to.not.exist | ||
249 | expect(videos[0].state).to.not.exist | ||
250 | expect(videos[0].waitTranscoding).to.not.exist | ||
251 | } | ||
252 | |||
253 | { | ||
254 | const videos = await listVideos({ server: servers[0], path, include: VideoInclude.NOT_PUBLISHED_STATE }) | ||
255 | const video = finder(videos) | ||
256 | expect(video).to.exist | ||
257 | expect(video.state).to.exist | ||
258 | } | ||
259 | } | ||
260 | }) | ||
261 | |||
262 | it('Should include blacklisted videos', async function () { | ||
263 | const { id } = await servers[0].videos.upload({ attributes: { name: 'blacklisted' } }) | ||
264 | |||
265 | await servers[0].blacklist.add({ videoId: id }) | ||
266 | |||
267 | const finder = (videos: Video[]) => videos.find(v => v.name === 'blacklisted') | ||
268 | |||
269 | for (const path of paths) { | ||
270 | { | ||
271 | const videos = await listVideos({ server: servers[0], path }) | ||
272 | const video = finder(videos) | ||
273 | expect(video).to.not.exist | ||
274 | expect(videos[0].blacklisted).to.not.exist | ||
275 | } | ||
276 | |||
277 | { | ||
278 | const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLACKLISTED }) | ||
279 | const video = finder(videos) | ||
280 | expect(video).to.exist | ||
281 | expect(video.blacklisted).to.be.true | ||
282 | } | ||
283 | } | ||
284 | }) | ||
285 | |||
286 | it('Should include videos from muted account', async function () { | ||
287 | const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video') | ||
288 | |||
289 | await servers[0].blocklist.addToServerBlocklist({ account: 'root@' + servers[1].host }) | ||
290 | |||
291 | for (const path of remotePaths) { | ||
292 | { | ||
293 | const videos = await listVideos({ server: servers[0], path }) | ||
294 | const video = finder(videos) | ||
295 | expect(video).to.not.exist | ||
296 | |||
297 | // Some paths won't have videos | ||
298 | if (videos[0]) { | ||
299 | expect(videos[0].blockedOwner).to.not.exist | ||
300 | expect(videos[0].blockedServer).to.not.exist | ||
301 | } | ||
302 | } | ||
303 | |||
304 | { | ||
305 | const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER }) | ||
306 | |||
307 | const video = finder(videos) | ||
308 | expect(video).to.exist | ||
309 | expect(video.blockedServer).to.be.false | ||
310 | expect(video.blockedOwner).to.be.true | ||
311 | } | ||
312 | } | ||
313 | |||
314 | await servers[0].blocklist.removeFromServerBlocklist({ account: 'root@' + servers[1].host }) | ||
315 | }) | ||
316 | |||
317 | it('Should include videos from muted server', async function () { | ||
318 | const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video') | ||
319 | |||
320 | await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host }) | ||
321 | |||
322 | for (const path of remotePaths) { | ||
323 | { | ||
324 | const videos = await listVideos({ server: servers[0], path }) | ||
325 | const video = finder(videos) | ||
326 | expect(video).to.not.exist | ||
327 | |||
328 | // Some paths won't have videos | ||
329 | if (videos[0]) { | ||
330 | expect(videos[0].blockedOwner).to.not.exist | ||
331 | expect(videos[0].blockedServer).to.not.exist | ||
332 | } | ||
333 | } | ||
334 | |||
335 | { | ||
336 | const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER }) | ||
337 | const video = finder(videos) | ||
338 | expect(video).to.exist | ||
339 | expect(video.blockedServer).to.be.true | ||
340 | expect(video.blockedOwner).to.be.false | ||
341 | } | ||
342 | } | ||
343 | |||
344 | await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host }) | ||
345 | }) | ||
346 | |||
347 | it('Should include video files', async function () { | ||
348 | for (const path of paths) { | ||
349 | { | ||
350 | const videos = await listVideos({ server: servers[0], path }) | ||
351 | |||
352 | for (const video of videos) { | ||
353 | const videoWithFiles = video as VideoDetails | ||
354 | |||
355 | expect(videoWithFiles.files).to.not.exist | ||
356 | expect(videoWithFiles.streamingPlaylists).to.not.exist | ||
357 | } | ||
358 | } | ||
359 | |||
360 | { | ||
361 | const videos = await listVideos({ server: servers[0], path, include: VideoInclude.FILES }) | ||
362 | |||
363 | for (const video of videos) { | ||
364 | const videoWithFiles = video as VideoDetails | ||
365 | |||
366 | expect(videoWithFiles.files).to.exist | ||
367 | expect(videoWithFiles.files).to.have.length.at.least(1) | ||
368 | } | ||
369 | } | ||
370 | } | ||
371 | }) | ||
372 | |||
373 | it('Should filter by tags and category', async function () { | ||
374 | await servers[0].videos.upload({ attributes: { name: 'tag filter', tags: [ 'tag1', 'tag2' ] } }) | ||
375 | await servers[0].videos.upload({ attributes: { name: 'tag filter with category', tags: [ 'tag3' ], category: 4 } }) | ||
376 | |||
377 | for (const path of paths) { | ||
378 | { | ||
379 | const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] }) | ||
380 | expect(videos).to.have.lengthOf(1) | ||
381 | expect(videos[0].name).to.equal('tag filter') | ||
382 | } | ||
383 | |||
384 | { | ||
385 | const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag3' ] }) | ||
386 | expect(videos).to.have.lengthOf(0) | ||
387 | } | ||
388 | |||
389 | { | ||
390 | const { data, total } = await servers[0].videos.list({ tagsAllOf: [ 'tag3' ], categoryOneOf: [ 4 ] }) | ||
391 | expect(total).to.equal(1) | ||
392 | expect(data[0].name).to.equal('tag filter with category') | ||
393 | } | ||
394 | |||
395 | { | ||
396 | const { total } = await servers[0].videos.list({ tagsAllOf: [ 'tag4' ], categoryOneOf: [ 4 ] }) | ||
397 | expect(total).to.equal(0) | ||
398 | } | ||
399 | } | ||
400 | }) | ||
401 | |||
402 | it('Should filter by HLS or Web Video files', async function () { | ||
403 | this.timeout(360000) | ||
404 | |||
405 | const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name) | ||
406 | |||
407 | await servers[0].config.enableTranscoding({ hls: false, webVideo: true }) | ||
408 | await servers[0].videos.upload({ attributes: { name: 'web video' } }) | ||
409 | const hasWebVideo = finderFactory('web video') | ||
410 | |||
411 | await waitJobs(servers) | ||
412 | |||
413 | await servers[0].config.enableTranscoding({ hls: true, webVideo: false }) | ||
414 | await servers[0].videos.upload({ attributes: { name: 'hls video' } }) | ||
415 | const hasHLS = finderFactory('hls video') | ||
416 | |||
417 | await waitJobs(servers) | ||
418 | |||
419 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) | ||
420 | await servers[0].videos.upload({ attributes: { name: 'hls and web video' } }) | ||
421 | const hasBoth = finderFactory('hls and web video') | ||
422 | |||
423 | await waitJobs(servers) | ||
424 | |||
425 | for (const path of paths) { | ||
426 | { | ||
427 | const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: true }) | ||
428 | |||
429 | expect(hasWebVideo(videos)).to.be.true | ||
430 | expect(hasHLS(videos)).to.be.false | ||
431 | expect(hasBoth(videos)).to.be.true | ||
432 | } | ||
433 | |||
434 | { | ||
435 | const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: false }) | ||
436 | |||
437 | expect(hasWebVideo(videos)).to.be.false | ||
438 | expect(hasHLS(videos)).to.be.true | ||
439 | expect(hasBoth(videos)).to.be.false | ||
440 | } | ||
441 | |||
442 | { | ||
443 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true }) | ||
444 | |||
445 | expect(hasWebVideo(videos)).to.be.false | ||
446 | expect(hasHLS(videos)).to.be.true | ||
447 | expect(hasBoth(videos)).to.be.true | ||
448 | } | ||
449 | |||
450 | { | ||
451 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false }) | ||
452 | |||
453 | expect(hasWebVideo(videos)).to.be.true | ||
454 | expect(hasHLS(videos)).to.be.false | ||
455 | expect(hasBoth(videos)).to.be.false | ||
456 | } | ||
457 | |||
458 | { | ||
459 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebVideoFiles: false }) | ||
460 | |||
461 | expect(hasWebVideo(videos)).to.be.false | ||
462 | expect(hasHLS(videos)).to.be.false | ||
463 | expect(hasBoth(videos)).to.be.false | ||
464 | } | ||
465 | |||
466 | { | ||
467 | const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebVideoFiles: true }) | ||
468 | |||
469 | expect(hasWebVideo(videos)).to.be.false | ||
470 | expect(hasHLS(videos)).to.be.false | ||
471 | expect(hasBoth(videos)).to.be.true | ||
472 | } | ||
473 | } | ||
474 | }) | ||
475 | |||
476 | it('Should filter already watched videos by the user', async function () { | ||
477 | const { id } = await servers[0].videos.upload({ attributes: { name: 'video for history' } }) | ||
478 | |||
479 | for (const path of paths) { | ||
480 | const videos = await listVideos({ server: servers[0], path, isLocal: true, excludeAlreadyWatched: true }) | ||
481 | const foundVideo = videos.find(video => video.id === id) | ||
482 | |||
483 | expect(foundVideo).to.not.be.undefined | ||
484 | } | ||
485 | await servers[0].views.view({ id, currentTime: 1, token: servers[0].accessToken }) | ||
486 | |||
487 | for (const path of paths) { | ||
488 | const videos = await listVideos({ server: servers[0], path, excludeAlreadyWatched: true }) | ||
489 | const foundVideo = videos.find(video => video.id === id) | ||
490 | |||
491 | expect(foundVideo).to.be.undefined | ||
492 | } | ||
493 | }) | ||
494 | }) | ||
495 | |||
496 | after(async function () { | ||
497 | await cleanupTests(servers) | ||
498 | }) | ||
499 | }) | ||
diff --git a/packages/tests/src/api/videos/videos-history.ts b/packages/tests/src/api/videos/videos-history.ts new file mode 100644 index 000000000..75c0fcebd --- /dev/null +++ b/packages/tests/src/api/videos/videos-history.ts | |||
@@ -0,0 +1,230 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { Video } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | killallServers, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test videos history', function () { | ||
15 | let server: PeerTubeServer = null | ||
16 | let video1Id: number | ||
17 | let video1UUID: string | ||
18 | let video2UUID: string | ||
19 | let video3UUID: string | ||
20 | let video3WatchedDate: Date | ||
21 | let userAccessToken: string | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(120000) | ||
25 | |||
26 | server = await createSingleServer(1) | ||
27 | |||
28 | await setAccessTokensToServers([ server ]) | ||
29 | |||
30 | // 10 seconds long | ||
31 | const fixture = 'video_short1.webm' | ||
32 | |||
33 | { | ||
34 | const { id, uuid } = await server.videos.upload({ attributes: { name: 'video 1', fixture } }) | ||
35 | video1UUID = uuid | ||
36 | video1Id = id | ||
37 | } | ||
38 | |||
39 | { | ||
40 | const { uuid } = await server.videos.upload({ attributes: { name: 'video 2', fixture } }) | ||
41 | video2UUID = uuid | ||
42 | } | ||
43 | |||
44 | { | ||
45 | const { uuid } = await server.videos.upload({ attributes: { name: 'video 3', fixture } }) | ||
46 | video3UUID = uuid | ||
47 | } | ||
48 | |||
49 | userAccessToken = await server.users.generateUserAndToken('user_1') | ||
50 | }) | ||
51 | |||
52 | it('Should get videos, without watching history', async function () { | ||
53 | const { data } = await server.videos.listWithToken() | ||
54 | |||
55 | for (const video of data) { | ||
56 | const videoDetails = await server.videos.getWithToken({ id: video.id }) | ||
57 | |||
58 | expect(video.userHistory).to.be.undefined | ||
59 | expect(videoDetails.userHistory).to.be.undefined | ||
60 | } | ||
61 | }) | ||
62 | |||
63 | it('Should watch the first and second video', async function () { | ||
64 | await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 }) | ||
65 | await server.views.view({ id: video1UUID, token: server.accessToken, currentTime: 3 }) | ||
66 | }) | ||
67 | |||
68 | it('Should return the correct history when listing, searching and getting videos', async function () { | ||
69 | const videosOfVideos: Video[][] = [] | ||
70 | |||
71 | { | ||
72 | const { data } = await server.videos.listWithToken() | ||
73 | videosOfVideos.push(data) | ||
74 | } | ||
75 | |||
76 | { | ||
77 | const body = await server.search.searchVideos({ token: server.accessToken, search: 'video' }) | ||
78 | videosOfVideos.push(body.data) | ||
79 | } | ||
80 | |||
81 | for (const videos of videosOfVideos) { | ||
82 | const video1 = videos.find(v => v.uuid === video1UUID) | ||
83 | const video2 = videos.find(v => v.uuid === video2UUID) | ||
84 | const video3 = videos.find(v => v.uuid === video3UUID) | ||
85 | |||
86 | expect(video1.userHistory).to.not.be.undefined | ||
87 | expect(video1.userHistory.currentTime).to.equal(3) | ||
88 | |||
89 | expect(video2.userHistory).to.not.be.undefined | ||
90 | expect(video2.userHistory.currentTime).to.equal(8) | ||
91 | |||
92 | expect(video3.userHistory).to.be.undefined | ||
93 | } | ||
94 | |||
95 | { | ||
96 | const videoDetails = await server.videos.getWithToken({ id: video1UUID }) | ||
97 | |||
98 | expect(videoDetails.userHistory).to.not.be.undefined | ||
99 | expect(videoDetails.userHistory.currentTime).to.equal(3) | ||
100 | } | ||
101 | |||
102 | { | ||
103 | const videoDetails = await server.videos.getWithToken({ id: video2UUID }) | ||
104 | |||
105 | expect(videoDetails.userHistory).to.not.be.undefined | ||
106 | expect(videoDetails.userHistory.currentTime).to.equal(8) | ||
107 | } | ||
108 | |||
109 | { | ||
110 | const videoDetails = await server.videos.getWithToken({ id: video3UUID }) | ||
111 | |||
112 | expect(videoDetails.userHistory).to.be.undefined | ||
113 | } | ||
114 | }) | ||
115 | |||
116 | it('Should have these videos when listing my history', async function () { | ||
117 | video3WatchedDate = new Date() | ||
118 | await server.views.view({ id: video3UUID, token: server.accessToken, currentTime: 2 }) | ||
119 | |||
120 | const body = await server.history.list() | ||
121 | |||
122 | expect(body.total).to.equal(3) | ||
123 | |||
124 | const videos = body.data | ||
125 | expect(videos[0].name).to.equal('video 3') | ||
126 | expect(videos[1].name).to.equal('video 1') | ||
127 | expect(videos[2].name).to.equal('video 2') | ||
128 | }) | ||
129 | |||
130 | it('Should not have videos history on another user', async function () { | ||
131 | const body = await server.history.list({ token: userAccessToken }) | ||
132 | |||
133 | expect(body.total).to.equal(0) | ||
134 | expect(body.data).to.have.lengthOf(0) | ||
135 | }) | ||
136 | |||
137 | it('Should be able to search through videos in my history', async function () { | ||
138 | const body = await server.history.list({ search: '2' }) | ||
139 | expect(body.total).to.equal(1) | ||
140 | |||
141 | const videos = body.data | ||
142 | expect(videos[0].name).to.equal('video 2') | ||
143 | }) | ||
144 | |||
145 | it('Should clear my history', async function () { | ||
146 | await server.history.removeAll({ beforeDate: video3WatchedDate.toISOString() }) | ||
147 | }) | ||
148 | |||
149 | it('Should have my history cleared', async function () { | ||
150 | const body = await server.history.list() | ||
151 | expect(body.total).to.equal(1) | ||
152 | |||
153 | const videos = body.data | ||
154 | expect(videos[0].name).to.equal('video 3') | ||
155 | }) | ||
156 | |||
157 | it('Should disable videos history', async function () { | ||
158 | await server.users.updateMe({ | ||
159 | videosHistoryEnabled: false | ||
160 | }) | ||
161 | |||
162 | await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 }) | ||
163 | |||
164 | const { data } = await server.history.list() | ||
165 | expect(data[0].name).to.not.equal('video 2') | ||
166 | }) | ||
167 | |||
168 | it('Should re-enable videos history', async function () { | ||
169 | await server.users.updateMe({ | ||
170 | videosHistoryEnabled: true | ||
171 | }) | ||
172 | |||
173 | await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 }) | ||
174 | |||
175 | const { data } = await server.history.list() | ||
176 | expect(data[0].name).to.equal('video 2') | ||
177 | }) | ||
178 | |||
179 | it('Should not clean old history', async function () { | ||
180 | this.timeout(50000) | ||
181 | |||
182 | await killallServers([ server ]) | ||
183 | |||
184 | await server.run({ history: { videos: { max_age: '10 days' } } }) | ||
185 | |||
186 | await wait(6000) | ||
187 | |||
188 | // Should still have history | ||
189 | |||
190 | const body = await server.history.list() | ||
191 | expect(body.total).to.equal(2) | ||
192 | }) | ||
193 | |||
194 | it('Should clean old history', async function () { | ||
195 | this.timeout(50000) | ||
196 | |||
197 | await killallServers([ server ]) | ||
198 | |||
199 | await server.run({ history: { videos: { max_age: '5 seconds' } } }) | ||
200 | |||
201 | await wait(6000) | ||
202 | |||
203 | const body = await server.history.list() | ||
204 | expect(body.total).to.equal(0) | ||
205 | }) | ||
206 | |||
207 | it('Should delete a specific history element', async function () { | ||
208 | { | ||
209 | await server.views.view({ id: video1UUID, token: server.accessToken, currentTime: 4 }) | ||
210 | await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 }) | ||
211 | } | ||
212 | |||
213 | { | ||
214 | const body = await server.history.list() | ||
215 | expect(body.total).to.equal(2) | ||
216 | } | ||
217 | |||
218 | { | ||
219 | await server.history.removeElement({ videoId: video1Id }) | ||
220 | |||
221 | const body = await server.history.list() | ||
222 | expect(body.total).to.equal(1) | ||
223 | expect(body.data[0].uuid).to.equal(video2UUID) | ||
224 | } | ||
225 | }) | ||
226 | |||
227 | after(async function () { | ||
228 | await cleanupTests([ server ]) | ||
229 | }) | ||
230 | }) | ||
diff --git a/packages/tests/src/api/videos/videos-overview.ts b/packages/tests/src/api/videos/videos-overview.ts new file mode 100644 index 000000000..7d74d6db2 --- /dev/null +++ b/packages/tests/src/api/videos/videos-overview.ts | |||
@@ -0,0 +1,129 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideosOverview } from '@peertube/peertube-models' | ||
6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
7 | |||
8 | describe('Test a videos overview', function () { | ||
9 | let server: PeerTubeServer = null | ||
10 | |||
11 | function testOverviewCount (overview: VideosOverview, expected: number) { | ||
12 | expect(overview.tags).to.have.lengthOf(expected) | ||
13 | expect(overview.categories).to.have.lengthOf(expected) | ||
14 | expect(overview.channels).to.have.lengthOf(expected) | ||
15 | } | ||
16 | |||
17 | before(async function () { | ||
18 | this.timeout(30000) | ||
19 | |||
20 | server = await createSingleServer(1) | ||
21 | |||
22 | await setAccessTokensToServers([ server ]) | ||
23 | }) | ||
24 | |||
25 | it('Should send empty overview', async function () { | ||
26 | const body = await server.overviews.getVideos({ page: 1 }) | ||
27 | |||
28 | testOverviewCount(body, 0) | ||
29 | }) | ||
30 | |||
31 | it('Should upload 5 videos in a specific category, tag and channel but not include them in overview', async function () { | ||
32 | this.timeout(60000) | ||
33 | |||
34 | await wait(3000) | ||
35 | |||
36 | await server.videos.upload({ | ||
37 | attributes: { | ||
38 | name: 'video 0', | ||
39 | category: 3, | ||
40 | tags: [ 'coucou1', 'coucou2' ] | ||
41 | } | ||
42 | }) | ||
43 | |||
44 | const body = await server.overviews.getVideos({ page: 1 }) | ||
45 | |||
46 | testOverviewCount(body, 0) | ||
47 | }) | ||
48 | |||
49 | it('Should upload another video and include all videos in the overview', async function () { | ||
50 | this.timeout(120000) | ||
51 | |||
52 | { | ||
53 | for (let i = 1; i < 6; i++) { | ||
54 | await server.videos.upload({ | ||
55 | attributes: { | ||
56 | name: 'video ' + i, | ||
57 | category: 3, | ||
58 | tags: [ 'coucou1', 'coucou2' ] | ||
59 | } | ||
60 | }) | ||
61 | } | ||
62 | |||
63 | await wait(3000) | ||
64 | } | ||
65 | |||
66 | { | ||
67 | const body = await server.overviews.getVideos({ page: 1 }) | ||
68 | |||
69 | testOverviewCount(body, 1) | ||
70 | } | ||
71 | |||
72 | { | ||
73 | const overview = await server.overviews.getVideos({ page: 2 }) | ||
74 | |||
75 | expect(overview.tags).to.have.lengthOf(1) | ||
76 | expect(overview.categories).to.have.lengthOf(0) | ||
77 | expect(overview.channels).to.have.lengthOf(0) | ||
78 | } | ||
79 | }) | ||
80 | |||
81 | it('Should have the correct overview', async function () { | ||
82 | const overview1 = await server.overviews.getVideos({ page: 1 }) | ||
83 | const overview2 = await server.overviews.getVideos({ page: 2 }) | ||
84 | |||
85 | for (const arr of [ overview1.tags, overview1.categories, overview1.channels, overview2.tags ]) { | ||
86 | expect(arr).to.have.lengthOf(1) | ||
87 | |||
88 | const obj = arr[0] | ||
89 | |||
90 | expect(obj.videos).to.have.lengthOf(6) | ||
91 | expect(obj.videos[0].name).to.equal('video 5') | ||
92 | expect(obj.videos[1].name).to.equal('video 4') | ||
93 | expect(obj.videos[2].name).to.equal('video 3') | ||
94 | expect(obj.videos[3].name).to.equal('video 2') | ||
95 | expect(obj.videos[4].name).to.equal('video 1') | ||
96 | expect(obj.videos[5].name).to.equal('video 0') | ||
97 | } | ||
98 | |||
99 | const tags = [ overview1.tags[0].tag, overview2.tags[0].tag ] | ||
100 | expect(tags.find(t => t === 'coucou1')).to.not.be.undefined | ||
101 | expect(tags.find(t => t === 'coucou2')).to.not.be.undefined | ||
102 | |||
103 | expect(overview1.categories[0].category.id).to.equal(3) | ||
104 | |||
105 | expect(overview1.channels[0].channel.name).to.equal('root_channel') | ||
106 | }) | ||
107 | |||
108 | it('Should hide muted accounts', async function () { | ||
109 | const token = await server.users.generateUserAndToken('choco') | ||
110 | |||
111 | await server.blocklist.addToMyBlocklist({ token, account: 'root@' + server.host }) | ||
112 | |||
113 | { | ||
114 | const body = await server.overviews.getVideos({ page: 1 }) | ||
115 | |||
116 | testOverviewCount(body, 1) | ||
117 | } | ||
118 | |||
119 | { | ||
120 | const body = await server.overviews.getVideos({ page: 1, token }) | ||
121 | |||
122 | testOverviewCount(body, 0) | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | after(async function () { | ||
127 | await cleanupTests([ server ]) | ||
128 | }) | ||
129 | }) | ||
diff --git a/packages/tests/src/api/views/index.ts b/packages/tests/src/api/views/index.ts new file mode 100644 index 000000000..2b7334d1a --- /dev/null +++ b/packages/tests/src/api/views/index.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | export * from './video-views-counter.js' | ||
2 | export * from './video-views-overall-stats.js' | ||
3 | export * from './video-views-retention-stats.js' | ||
4 | export * from './video-views-timeserie-stats.js' | ||
5 | export * from './videos-views-cleaner.js' | ||
diff --git a/packages/tests/src/api/views/video-views-counter.ts b/packages/tests/src/api/views/video-views-counter.ts new file mode 100644 index 000000000..d9afb0f18 --- /dev/null +++ b/packages/tests/src/api/views/video-views-counter.ts | |||
@@ -0,0 +1,153 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@tests/shared/views.js' | ||
6 | import { wait } from '@peertube/peertube-core-utils' | ||
7 | import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | describe('Test video views/viewers counters', function () { | ||
10 | let servers: PeerTubeServer[] | ||
11 | |||
12 | async function checkCounter (field: 'views' | 'viewers', id: string, expected: number) { | ||
13 | for (const server of servers) { | ||
14 | const video = await server.videos.get({ id }) | ||
15 | |||
16 | const messageSuffix = video.isLive | ||
17 | ? 'live video' | ||
18 | : 'vod video' | ||
19 | |||
20 | expect(video[field]).to.equal(expected, `${field} not valid on server ${server.serverNumber} for ${messageSuffix} ${video.uuid}`) | ||
21 | } | ||
22 | } | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(120000) | ||
26 | |||
27 | servers = await prepareViewsServers() | ||
28 | }) | ||
29 | |||
30 | describe('Test views counter on VOD', function () { | ||
31 | let videoUUID: string | ||
32 | |||
33 | before(async function () { | ||
34 | this.timeout(120000) | ||
35 | |||
36 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
37 | videoUUID = uuid | ||
38 | |||
39 | await waitJobs(servers) | ||
40 | }) | ||
41 | |||
42 | it('Should not view a video if watch time is below the threshold', async function () { | ||
43 | await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 2 ] }) | ||
44 | await processViewsBuffer(servers) | ||
45 | |||
46 | await checkCounter('views', videoUUID, 0) | ||
47 | }) | ||
48 | |||
49 | it('Should view a video if watch time is above the threshold', async function () { | ||
50 | await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] }) | ||
51 | await processViewsBuffer(servers) | ||
52 | |||
53 | await checkCounter('views', videoUUID, 1) | ||
54 | }) | ||
55 | |||
56 | it('Should not view again this video with the same IP', async function () { | ||
57 | await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 1, 4 ] }) | ||
58 | await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 1, 4 ] }) | ||
59 | await processViewsBuffer(servers) | ||
60 | |||
61 | await checkCounter('views', videoUUID, 2) | ||
62 | }) | ||
63 | |||
64 | it('Should view the video from server 2 and send the event', async function () { | ||
65 | await servers[1].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] }) | ||
66 | await waitJobs(servers) | ||
67 | await processViewsBuffer(servers) | ||
68 | |||
69 | await checkCounter('views', videoUUID, 3) | ||
70 | }) | ||
71 | }) | ||
72 | |||
73 | describe('Test views and viewers counters on live and VOD', function () { | ||
74 | let liveVideoId: string | ||
75 | let vodVideoId: string | ||
76 | let command: FfmpegCommand | ||
77 | |||
78 | before(async function () { | ||
79 | this.timeout(240000); | ||
80 | |||
81 | ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true })) | ||
82 | }) | ||
83 | |||
84 | it('Should display no views and viewers', async function () { | ||
85 | await checkCounter('views', liveVideoId, 0) | ||
86 | await checkCounter('viewers', liveVideoId, 0) | ||
87 | |||
88 | await checkCounter('views', vodVideoId, 0) | ||
89 | await checkCounter('viewers', vodVideoId, 0) | ||
90 | }) | ||
91 | |||
92 | it('Should view twice and display 1 view/viewer', async function () { | ||
93 | this.timeout(30000) | ||
94 | |||
95 | await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] }) | ||
96 | await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] }) | ||
97 | await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] }) | ||
98 | await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] }) | ||
99 | |||
100 | await waitJobs(servers) | ||
101 | await checkCounter('viewers', liveVideoId, 1) | ||
102 | await checkCounter('viewers', vodVideoId, 1) | ||
103 | |||
104 | await processViewsBuffer(servers) | ||
105 | |||
106 | await checkCounter('views', liveVideoId, 1) | ||
107 | await checkCounter('views', vodVideoId, 1) | ||
108 | }) | ||
109 | |||
110 | it('Should wait and display 0 viewers but still have 1 view', async function () { | ||
111 | this.timeout(30000) | ||
112 | |||
113 | await wait(12000) | ||
114 | await waitJobs(servers) | ||
115 | |||
116 | await checkCounter('views', liveVideoId, 1) | ||
117 | await checkCounter('viewers', liveVideoId, 0) | ||
118 | |||
119 | await checkCounter('views', vodVideoId, 1) | ||
120 | await checkCounter('viewers', vodVideoId, 0) | ||
121 | }) | ||
122 | |||
123 | it('Should view on a remote and on local and display 2 viewers and 3 views', async function () { | ||
124 | this.timeout(30000) | ||
125 | |||
126 | await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] }) | ||
127 | await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] }) | ||
128 | await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] }) | ||
129 | |||
130 | await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] }) | ||
131 | await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] }) | ||
132 | await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] }) | ||
133 | |||
134 | await waitJobs(servers) | ||
135 | |||
136 | await checkCounter('viewers', liveVideoId, 2) | ||
137 | await checkCounter('viewers', vodVideoId, 2) | ||
138 | |||
139 | await processViewsBuffer(servers) | ||
140 | |||
141 | await checkCounter('views', liveVideoId, 3) | ||
142 | await checkCounter('views', vodVideoId, 3) | ||
143 | }) | ||
144 | |||
145 | after(async function () { | ||
146 | await stopFfmpeg(command) | ||
147 | }) | ||
148 | }) | ||
149 | |||
150 | after(async function () { | ||
151 | await cleanupTests(servers) | ||
152 | }) | ||
153 | }) | ||
diff --git a/packages/tests/src/api/views/video-views-overall-stats.ts b/packages/tests/src/api/views/video-views-overall-stats.ts new file mode 100644 index 000000000..6ea0da2d9 --- /dev/null +++ b/packages/tests/src/api/views/video-views-overall-stats.ts | |||
@@ -0,0 +1,368 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
6 | import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' | ||
7 | import { wait } from '@peertube/peertube-core-utils' | ||
8 | import { VideoStatsOverall } from '@peertube/peertube-models' | ||
9 | |||
10 | /** | ||
11 | * | ||
12 | * Simulate 5 sections of viewers | ||
13 | * * user0 started and ended before start date | ||
14 | * * user1 started before start date and ended in the interval | ||
15 | * * user2 started started in the interval and ended after end date | ||
16 | * * user3 started and ended in the interval | ||
17 | * * user4 started and ended after end date | ||
18 | */ | ||
19 | async function simulateComplexViewers (servers: PeerTubeServer[], videoUUID: string) { | ||
20 | const user0 = '8.8.8.8,127.0.0.1' | ||
21 | const user1 = '8.8.8.8,127.0.0.1' | ||
22 | const user2 = '8.8.8.9,127.0.0.1' | ||
23 | const user3 = '8.8.8.10,127.0.0.1' | ||
24 | const user4 = '8.8.8.11,127.0.0.1' | ||
25 | |||
26 | await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user0 }) // User 0 starts | ||
27 | await wait(500) | ||
28 | |||
29 | await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user1 }) // User 1 starts | ||
30 | await servers[0].views.view({ id: videoUUID, currentTime: 2, xForwardedFor: user0 }) // User 0 ends | ||
31 | await wait(500) | ||
32 | |||
33 | const startDate = new Date().toISOString() | ||
34 | await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user2 }) // User 2 starts | ||
35 | await wait(500) | ||
36 | |||
37 | await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user3 }) // User 3 starts | ||
38 | await wait(500) | ||
39 | |||
40 | await servers[0].views.view({ id: videoUUID, currentTime: 4, xForwardedFor: user1 }) // User 1 ends | ||
41 | await wait(500) | ||
42 | |||
43 | await servers[0].views.view({ id: videoUUID, currentTime: 3, xForwardedFor: user3 }) // User 3 ends | ||
44 | await wait(500) | ||
45 | |||
46 | const endDate = new Date().toISOString() | ||
47 | await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user4 }) // User 4 starts | ||
48 | await servers[0].views.view({ id: videoUUID, currentTime: 5, xForwardedFor: user2 }) // User 2 ends | ||
49 | await wait(500) | ||
50 | |||
51 | await servers[0].views.view({ id: videoUUID, currentTime: 1, xForwardedFor: user4 }) // User 4 ends | ||
52 | |||
53 | await processViewersStats(servers) | ||
54 | |||
55 | return { startDate, endDate } | ||
56 | } | ||
57 | |||
58 | describe('Test views overall stats', function () { | ||
59 | let servers: PeerTubeServer[] | ||
60 | |||
61 | before(async function () { | ||
62 | this.timeout(120000) | ||
63 | |||
64 | servers = await prepareViewsServers() | ||
65 | }) | ||
66 | |||
67 | describe('Test watch time stats of local videos on live and VOD', function () { | ||
68 | let vodVideoId: string | ||
69 | let liveVideoId: string | ||
70 | let command: FfmpegCommand | ||
71 | |||
72 | before(async function () { | ||
73 | this.timeout(240000); | ||
74 | |||
75 | ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true })) | ||
76 | }) | ||
77 | |||
78 | it('Should display overall stats of a video with no viewers', async function () { | ||
79 | for (const videoId of [ liveVideoId, vodVideoId ]) { | ||
80 | const stats = await servers[0].videoStats.getOverallStats({ videoId }) | ||
81 | const video = await servers[0].videos.get({ id: videoId }) | ||
82 | |||
83 | expect(video.views).to.equal(0) | ||
84 | expect(stats.averageWatchTime).to.equal(0) | ||
85 | expect(stats.totalWatchTime).to.equal(0) | ||
86 | expect(stats.totalViewers).to.equal(0) | ||
87 | } | ||
88 | }) | ||
89 | |||
90 | it('Should display overall stats with 1 viewer below the watch time limit', async function () { | ||
91 | this.timeout(60000) | ||
92 | |||
93 | for (const videoId of [ liveVideoId, vodVideoId ]) { | ||
94 | await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] }) | ||
95 | } | ||
96 | |||
97 | await processViewersStats(servers) | ||
98 | |||
99 | for (const videoId of [ liveVideoId, vodVideoId ]) { | ||
100 | const stats = await servers[0].videoStats.getOverallStats({ videoId }) | ||
101 | const video = await servers[0].videos.get({ id: videoId }) | ||
102 | |||
103 | expect(video.views).to.equal(0) | ||
104 | expect(stats.averageWatchTime).to.equal(1) | ||
105 | expect(stats.totalWatchTime).to.equal(1) | ||
106 | expect(stats.totalViewers).to.equal(1) | ||
107 | } | ||
108 | }) | ||
109 | |||
110 | it('Should display overall stats with 2 viewers', async function () { | ||
111 | this.timeout(60000) | ||
112 | |||
113 | { | ||
114 | await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] }) | ||
115 | await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35, 40 ] }) | ||
116 | |||
117 | await processViewersStats(servers) | ||
118 | |||
119 | { | ||
120 | const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId }) | ||
121 | const video = await servers[0].videos.get({ id: vodVideoId }) | ||
122 | |||
123 | expect(video.views).to.equal(1) | ||
124 | expect(stats.averageWatchTime).to.equal(2) | ||
125 | expect(stats.totalWatchTime).to.equal(4) | ||
126 | expect(stats.totalViewers).to.equal(2) | ||
127 | } | ||
128 | |||
129 | { | ||
130 | const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId }) | ||
131 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
132 | |||
133 | expect(video.views).to.equal(1) | ||
134 | expect(stats.averageWatchTime).to.equal(21) | ||
135 | expect(stats.totalWatchTime).to.equal(41) | ||
136 | expect(stats.totalViewers).to.equal(2) | ||
137 | } | ||
138 | } | ||
139 | }) | ||
140 | |||
141 | it('Should display overall stats with a remote viewer below the watch time limit', async function () { | ||
142 | this.timeout(60000) | ||
143 | |||
144 | for (const videoId of [ liveVideoId, vodVideoId ]) { | ||
145 | await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 2 ] }) | ||
146 | } | ||
147 | |||
148 | await processViewersStats(servers) | ||
149 | |||
150 | { | ||
151 | const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId }) | ||
152 | const video = await servers[0].videos.get({ id: vodVideoId }) | ||
153 | |||
154 | expect(video.views).to.equal(1) | ||
155 | expect(stats.averageWatchTime).to.equal(2) | ||
156 | expect(stats.totalWatchTime).to.equal(6) | ||
157 | expect(stats.totalViewers).to.equal(3) | ||
158 | } | ||
159 | |||
160 | { | ||
161 | const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId }) | ||
162 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
163 | |||
164 | expect(video.views).to.equal(1) | ||
165 | expect(stats.averageWatchTime).to.equal(14) | ||
166 | expect(stats.totalWatchTime).to.equal(43) | ||
167 | expect(stats.totalViewers).to.equal(3) | ||
168 | } | ||
169 | }) | ||
170 | |||
171 | it('Should display overall stats with a remote viewer above the watch time limit', async function () { | ||
172 | this.timeout(60000) | ||
173 | |||
174 | await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] }) | ||
175 | await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 45 ] }) | ||
176 | await processViewersStats(servers) | ||
177 | |||
178 | { | ||
179 | const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId }) | ||
180 | const video = await servers[0].videos.get({ id: vodVideoId }) | ||
181 | |||
182 | expect(video.views).to.equal(2) | ||
183 | expect(stats.averageWatchTime).to.equal(3) | ||
184 | expect(stats.totalWatchTime).to.equal(11) | ||
185 | expect(stats.totalViewers).to.equal(4) | ||
186 | } | ||
187 | |||
188 | { | ||
189 | const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId }) | ||
190 | const video = await servers[0].videos.get({ id: liveVideoId }) | ||
191 | |||
192 | expect(video.views).to.equal(2) | ||
193 | expect(stats.averageWatchTime).to.equal(22) | ||
194 | expect(stats.totalWatchTime).to.equal(88) | ||
195 | expect(stats.totalViewers).to.equal(4) | ||
196 | } | ||
197 | }) | ||
198 | |||
199 | it('Should filter overall stats by date', async function () { | ||
200 | this.timeout(60000) | ||
201 | |||
202 | const beforeView = new Date() | ||
203 | |||
204 | await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] }) | ||
205 | await processViewersStats(servers) | ||
206 | |||
207 | { | ||
208 | const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId, startDate: beforeView.toISOString() }) | ||
209 | expect(stats.averageWatchTime).to.equal(3) | ||
210 | expect(stats.totalWatchTime).to.equal(3) | ||
211 | expect(stats.totalViewers).to.equal(1) | ||
212 | } | ||
213 | |||
214 | { | ||
215 | const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId, endDate: beforeView.toISOString() }) | ||
216 | expect(stats.averageWatchTime).to.equal(22) | ||
217 | expect(stats.totalWatchTime).to.equal(88) | ||
218 | expect(stats.totalViewers).to.equal(4) | ||
219 | } | ||
220 | }) | ||
221 | |||
222 | after(async function () { | ||
223 | await stopFfmpeg(command) | ||
224 | }) | ||
225 | }) | ||
226 | |||
227 | describe('Test watchers peak stats of local videos on VOD', function () { | ||
228 | let videoUUID: string | ||
229 | let before2Watchers: Date | ||
230 | |||
231 | before(async function () { | ||
232 | this.timeout(240000); | ||
233 | |||
234 | ({ vodVideoId: videoUUID } = await prepareViewsVideos({ servers, live: true, vod: true })) | ||
235 | }) | ||
236 | |||
237 | it('Should not have watchers peak', async function () { | ||
238 | const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID }) | ||
239 | |||
240 | expect(stats.viewersPeak).to.equal(0) | ||
241 | expect(stats.viewersPeakDate).to.be.null | ||
242 | }) | ||
243 | |||
244 | it('Should have watcher peak with 1 watcher', async function () { | ||
245 | this.timeout(60000) | ||
246 | |||
247 | const before = new Date() | ||
248 | await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 0, 2 ] }) | ||
249 | const after = new Date() | ||
250 | |||
251 | await processViewersStats(servers) | ||
252 | |||
253 | const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID }) | ||
254 | |||
255 | expect(stats.viewersPeak).to.equal(1) | ||
256 | expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after) | ||
257 | }) | ||
258 | |||
259 | it('Should have watcher peak with 2 watchers', async function () { | ||
260 | this.timeout(60000) | ||
261 | |||
262 | before2Watchers = new Date() | ||
263 | await servers[0].views.view({ id: videoUUID, currentTime: 0 }) | ||
264 | await servers[1].views.view({ id: videoUUID, currentTime: 0 }) | ||
265 | await servers[0].views.view({ id: videoUUID, currentTime: 2 }) | ||
266 | await servers[1].views.view({ id: videoUUID, currentTime: 2 }) | ||
267 | const after = new Date() | ||
268 | |||
269 | await processViewersStats(servers) | ||
270 | |||
271 | const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID }) | ||
272 | |||
273 | expect(stats.viewersPeak).to.equal(2) | ||
274 | expect(new Date(stats.viewersPeakDate)).to.be.above(before2Watchers).and.below(after) | ||
275 | }) | ||
276 | |||
277 | it('Should filter peak viewers stats by date', async function () { | ||
278 | { | ||
279 | const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() }) | ||
280 | expect(stats.viewersPeak).to.equal(0) | ||
281 | expect(stats.viewersPeakDate).to.not.exist | ||
282 | } | ||
283 | |||
284 | { | ||
285 | const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate: before2Watchers.toISOString() }) | ||
286 | expect(stats.viewersPeak).to.equal(1) | ||
287 | expect(new Date(stats.viewersPeakDate)).to.be.below(before2Watchers) | ||
288 | } | ||
289 | }) | ||
290 | |||
291 | it('Should complex filter peak viewers by date', async function () { | ||
292 | this.timeout(60000) | ||
293 | |||
294 | const { startDate, endDate } = await simulateComplexViewers(servers, videoUUID) | ||
295 | |||
296 | const expectCorrect = (stats: VideoStatsOverall) => { | ||
297 | expect(stats.viewersPeak).to.equal(3) | ||
298 | expect(new Date(stats.viewersPeakDate)).to.be.above(new Date(startDate)).and.below(new Date(endDate)) | ||
299 | } | ||
300 | |||
301 | expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate, endDate })) | ||
302 | expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate })) | ||
303 | expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate })) | ||
304 | expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID })) | ||
305 | }) | ||
306 | }) | ||
307 | |||
308 | describe('Test countries', function () { | ||
309 | let videoUUID: string | ||
310 | |||
311 | it('Should not report countries if geoip is disabled', async function () { | ||
312 | this.timeout(120000) | ||
313 | |||
314 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
315 | await waitJobs(servers) | ||
316 | |||
317 | await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 }) | ||
318 | |||
319 | await processViewersStats(servers) | ||
320 | |||
321 | const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid }) | ||
322 | expect(stats.countries).to.have.lengthOf(0) | ||
323 | }) | ||
324 | |||
325 | it('Should report countries if geoip is enabled', async function () { | ||
326 | this.timeout(240000) | ||
327 | |||
328 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
329 | videoUUID = uuid | ||
330 | await waitJobs(servers) | ||
331 | |||
332 | await Promise.all([ | ||
333 | servers[0].kill(), | ||
334 | servers[1].kill() | ||
335 | ]) | ||
336 | |||
337 | const config = { geo_ip: { enabled: true } } | ||
338 | await Promise.all([ | ||
339 | servers[0].run(config), | ||
340 | servers[1].run(config) | ||
341 | ]) | ||
342 | |||
343 | await servers[0].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 }) | ||
344 | await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.4,127.0.0.1', currentTime: 3 }) | ||
345 | await servers[1].views.view({ id: uuid, xForwardedFor: '80.67.169.12,127.0.0.1', currentTime: 2 }) | ||
346 | |||
347 | await processViewersStats(servers) | ||
348 | |||
349 | const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid }) | ||
350 | expect(stats.countries).to.have.lengthOf(2) | ||
351 | |||
352 | expect(stats.countries[0].isoCode).to.equal('US') | ||
353 | expect(stats.countries[0].viewers).to.equal(2) | ||
354 | |||
355 | expect(stats.countries[1].isoCode).to.equal('FR') | ||
356 | expect(stats.countries[1].viewers).to.equal(1) | ||
357 | }) | ||
358 | |||
359 | it('Should filter countries stats by date', async function () { | ||
360 | const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() }) | ||
361 | expect(stats.countries).to.have.lengthOf(0) | ||
362 | }) | ||
363 | }) | ||
364 | |||
365 | after(async function () { | ||
366 | await cleanupTests(servers) | ||
367 | }) | ||
368 | }) | ||
diff --git a/packages/tests/src/api/views/video-views-retention-stats.ts b/packages/tests/src/api/views/video-views-retention-stats.ts new file mode 100644 index 000000000..4cd0c7da9 --- /dev/null +++ b/packages/tests/src/api/views/video-views-retention-stats.ts | |||
@@ -0,0 +1,53 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
5 | import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
6 | |||
7 | describe('Test views retention stats', function () { | ||
8 | let servers: PeerTubeServer[] | ||
9 | |||
10 | before(async function () { | ||
11 | this.timeout(120000) | ||
12 | |||
13 | servers = await prepareViewsServers() | ||
14 | }) | ||
15 | |||
16 | describe('Test retention stats on VOD', function () { | ||
17 | let vodVideoId: string | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(240000); | ||
21 | |||
22 | ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true })) | ||
23 | }) | ||
24 | |||
25 | it('Should display empty retention', async function () { | ||
26 | const { data } = await servers[0].videoStats.getRetentionStats({ videoId: vodVideoId }) | ||
27 | expect(data).to.have.lengthOf(6) | ||
28 | |||
29 | for (let i = 0; i < 6; i++) { | ||
30 | expect(data[i].second).to.equal(i) | ||
31 | expect(data[i].retentionPercent).to.equal(0) | ||
32 | } | ||
33 | }) | ||
34 | |||
35 | it('Should display appropriate retention metrics', async function () { | ||
36 | await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] }) | ||
37 | await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 3 ] }) | ||
38 | await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 4 ] }) | ||
39 | await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] }) | ||
40 | |||
41 | await processViewersStats(servers) | ||
42 | |||
43 | const { data } = await servers[0].videoStats.getRetentionStats({ videoId: vodVideoId }) | ||
44 | expect(data).to.have.lengthOf(6) | ||
45 | |||
46 | expect(data.map(d => d.retentionPercent)).to.deep.equal([ 50, 75, 25, 25, 25, 0 ]) | ||
47 | }) | ||
48 | }) | ||
49 | |||
50 | after(async function () { | ||
51 | await cleanupTests(servers) | ||
52 | }) | ||
53 | }) | ||
diff --git a/packages/tests/src/api/views/video-views-timeserie-stats.ts b/packages/tests/src/api/views/video-views-timeserie-stats.ts new file mode 100644 index 000000000..44fccb644 --- /dev/null +++ b/packages/tests/src/api/views/video-views-timeserie-stats.ts | |||
@@ -0,0 +1,253 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
6 | import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@peertube/peertube-models' | ||
7 | import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | function buildOneMonthAgo () { | ||
10 | const monthAgo = new Date() | ||
11 | monthAgo.setHours(0, 0, 0, 0) | ||
12 | |||
13 | monthAgo.setDate(monthAgo.getDate() - 29) | ||
14 | |||
15 | return monthAgo | ||
16 | } | ||
17 | |||
18 | describe('Test views timeserie stats', function () { | ||
19 | const availableMetrics: VideoStatsTimeserieMetric[] = [ 'viewers' ] | ||
20 | |||
21 | let servers: PeerTubeServer[] | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(120000) | ||
25 | |||
26 | servers = await prepareViewsServers() | ||
27 | }) | ||
28 | |||
29 | describe('Common metric tests', function () { | ||
30 | let vodVideoId: string | ||
31 | |||
32 | before(async function () { | ||
33 | this.timeout(240000); | ||
34 | |||
35 | ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true })) | ||
36 | }) | ||
37 | |||
38 | it('Should display empty metric stats', async function () { | ||
39 | for (const metric of availableMetrics) { | ||
40 | const { data } = await servers[0].videoStats.getTimeserieStats({ videoId: vodVideoId, metric }) | ||
41 | |||
42 | expect(data).to.have.length.at.least(1) | ||
43 | |||
44 | for (const d of data) { | ||
45 | expect(d.value).to.equal(0) | ||
46 | } | ||
47 | } | ||
48 | }) | ||
49 | }) | ||
50 | |||
51 | describe('Test viewer and watch time metrics on live and VOD', function () { | ||
52 | let vodVideoId: string | ||
53 | let liveVideoId: string | ||
54 | let command: FfmpegCommand | ||
55 | |||
56 | function expectTodayLastValue (result: VideoStatsTimeserie, lastValue?: number) { | ||
57 | const { data } = result | ||
58 | |||
59 | const last = data[data.length - 1] | ||
60 | const today = new Date().getDate() | ||
61 | expect(new Date(last.date).getDate()).to.equal(today) | ||
62 | |||
63 | if (lastValue) expect(last.value).to.equal(lastValue) | ||
64 | } | ||
65 | |||
66 | function expectTimeserieData (result: VideoStatsTimeserie, lastValue: number) { | ||
67 | const { data } = result | ||
68 | expect(data).to.have.length.at.least(25) | ||
69 | |||
70 | expectTodayLastValue(result, lastValue) | ||
71 | |||
72 | for (let i = 0; i < data.length - 2; i++) { | ||
73 | expect(data[i].value).to.equal(0) | ||
74 | } | ||
75 | } | ||
76 | |||
77 | function expectInterval (result: VideoStatsTimeserie, intervalMs: number) { | ||
78 | const first = result.data[0] | ||
79 | const second = result.data[1] | ||
80 | expect(new Date(second.date).getTime() - new Date(first.date).getTime()).to.equal(intervalMs) | ||
81 | } | ||
82 | |||
83 | before(async function () { | ||
84 | this.timeout(240000); | ||
85 | |||
86 | ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true })) | ||
87 | }) | ||
88 | |||
89 | it('Should display appropriate viewers metrics', async function () { | ||
90 | for (const videoId of [ vodVideoId, liveVideoId ]) { | ||
91 | await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 3 ] }) | ||
92 | await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 5 ] }) | ||
93 | } | ||
94 | |||
95 | await processViewersStats(servers) | ||
96 | |||
97 | for (const videoId of [ vodVideoId, liveVideoId ]) { | ||
98 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
99 | videoId, | ||
100 | startDate: buildOneMonthAgo(), | ||
101 | endDate: new Date(), | ||
102 | metric: 'viewers' | ||
103 | }) | ||
104 | expectTimeserieData(result, 2) | ||
105 | } | ||
106 | }) | ||
107 | |||
108 | it('Should display appropriate watch time metrics', async function () { | ||
109 | for (const videoId of [ vodVideoId, liveVideoId ]) { | ||
110 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
111 | videoId, | ||
112 | startDate: buildOneMonthAgo(), | ||
113 | endDate: new Date(), | ||
114 | metric: 'aggregateWatchTime' | ||
115 | }) | ||
116 | expectTimeserieData(result, 8) | ||
117 | |||
118 | await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] }) | ||
119 | } | ||
120 | |||
121 | await processViewersStats(servers) | ||
122 | |||
123 | for (const videoId of [ vodVideoId, liveVideoId ]) { | ||
124 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
125 | videoId, | ||
126 | startDate: buildOneMonthAgo(), | ||
127 | endDate: new Date(), | ||
128 | metric: 'aggregateWatchTime' | ||
129 | }) | ||
130 | expectTimeserieData(result, 9) | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | it('Should use a custom start/end date', async function () { | ||
135 | const now = new Date() | ||
136 | const twentyDaysAgo = new Date() | ||
137 | twentyDaysAgo.setDate(twentyDaysAgo.getDate() - 19) | ||
138 | |||
139 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
140 | videoId: vodVideoId, | ||
141 | metric: 'aggregateWatchTime', | ||
142 | startDate: twentyDaysAgo, | ||
143 | endDate: now | ||
144 | }) | ||
145 | |||
146 | expect(result.groupInterval).to.equal('1 day') | ||
147 | expect(result.data).to.have.lengthOf(20) | ||
148 | |||
149 | const first = result.data[0] | ||
150 | expect(new Date(first.date).toLocaleDateString()).to.equal(twentyDaysAgo.toLocaleDateString()) | ||
151 | |||
152 | expectInterval(result, 24 * 3600 * 1000) | ||
153 | expectTodayLastValue(result, 9) | ||
154 | }) | ||
155 | |||
156 | it('Should automatically group by months', async function () { | ||
157 | const now = new Date() | ||
158 | const heightYearsAgo = new Date() | ||
159 | heightYearsAgo.setFullYear(heightYearsAgo.getFullYear() - 7) | ||
160 | |||
161 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
162 | videoId: vodVideoId, | ||
163 | metric: 'aggregateWatchTime', | ||
164 | startDate: heightYearsAgo, | ||
165 | endDate: now | ||
166 | }) | ||
167 | |||
168 | expect(result.groupInterval).to.equal('6 months') | ||
169 | expect(result.data).to.have.length.above(10).and.below(200) | ||
170 | }) | ||
171 | |||
172 | it('Should automatically group by days', async function () { | ||
173 | const now = new Date() | ||
174 | const threeMonthsAgo = new Date() | ||
175 | threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3) | ||
176 | |||
177 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
178 | videoId: vodVideoId, | ||
179 | metric: 'aggregateWatchTime', | ||
180 | startDate: threeMonthsAgo, | ||
181 | endDate: now | ||
182 | }) | ||
183 | |||
184 | expect(result.groupInterval).to.equal('2 days') | ||
185 | expect(result.data).to.have.length.above(10).and.below(200) | ||
186 | }) | ||
187 | |||
188 | it('Should automatically group by hours', async function () { | ||
189 | const now = new Date() | ||
190 | const twoDaysAgo = new Date() | ||
191 | twoDaysAgo.setDate(twoDaysAgo.getDate() - 1) | ||
192 | |||
193 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
194 | videoId: vodVideoId, | ||
195 | metric: 'aggregateWatchTime', | ||
196 | startDate: twoDaysAgo, | ||
197 | endDate: now | ||
198 | }) | ||
199 | |||
200 | expect(result.groupInterval).to.equal('1 hour') | ||
201 | expect(result.data).to.have.length.above(24).and.below(50) | ||
202 | |||
203 | expectInterval(result, 3600 * 1000) | ||
204 | expectTodayLastValue(result, 9) | ||
205 | }) | ||
206 | |||
207 | it('Should automatically group by ten minutes', async function () { | ||
208 | const now = new Date() | ||
209 | const twoHoursAgo = new Date() | ||
210 | twoHoursAgo.setHours(twoHoursAgo.getHours() - 4) | ||
211 | |||
212 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
213 | videoId: vodVideoId, | ||
214 | metric: 'aggregateWatchTime', | ||
215 | startDate: twoHoursAgo, | ||
216 | endDate: now | ||
217 | }) | ||
218 | |||
219 | expect(result.groupInterval).to.equal('10 minutes') | ||
220 | expect(result.data).to.have.length.above(20).and.below(30) | ||
221 | |||
222 | expectInterval(result, 60 * 10 * 1000) | ||
223 | expectTodayLastValue(result) | ||
224 | }) | ||
225 | |||
226 | it('Should automatically group by one minute', async function () { | ||
227 | const now = new Date() | ||
228 | const thirtyAgo = new Date() | ||
229 | thirtyAgo.setMinutes(thirtyAgo.getMinutes() - 30) | ||
230 | |||
231 | const result = await servers[0].videoStats.getTimeserieStats({ | ||
232 | videoId: vodVideoId, | ||
233 | metric: 'aggregateWatchTime', | ||
234 | startDate: thirtyAgo, | ||
235 | endDate: now | ||
236 | }) | ||
237 | |||
238 | expect(result.groupInterval).to.equal('1 minute') | ||
239 | expect(result.data).to.have.length.above(20).and.below(40) | ||
240 | |||
241 | expectInterval(result, 60 * 1000) | ||
242 | expectTodayLastValue(result) | ||
243 | }) | ||
244 | |||
245 | after(async function () { | ||
246 | await stopFfmpeg(command) | ||
247 | }) | ||
248 | }) | ||
249 | |||
250 | after(async function () { | ||
251 | await cleanupTests(servers) | ||
252 | }) | ||
253 | }) | ||
diff --git a/packages/tests/src/api/views/videos-views-cleaner.ts b/packages/tests/src/api/views/videos-views-cleaner.ts new file mode 100644 index 000000000..521dd9b5e --- /dev/null +++ b/packages/tests/src/api/views/videos-views-cleaner.ts | |||
@@ -0,0 +1,98 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('Test video views cleaner', function () { | ||
17 | let servers: PeerTubeServer[] | ||
18 | let sqlCommands: SQLCommand[] = [] | ||
19 | |||
20 | let videoIdServer1: string | ||
21 | let videoIdServer2: string | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(240000) | ||
25 | |||
26 | servers = await createMultipleServers(2) | ||
27 | await setAccessTokensToServers(servers) | ||
28 | |||
29 | await doubleFollow(servers[0], servers[1]) | ||
30 | |||
31 | videoIdServer1 = (await servers[0].videos.quickUpload({ name: 'video server 1' })).uuid | ||
32 | videoIdServer2 = (await servers[1].videos.quickUpload({ name: 'video server 2' })).uuid | ||
33 | |||
34 | await waitJobs(servers) | ||
35 | |||
36 | await servers[0].views.simulateView({ id: videoIdServer1 }) | ||
37 | await servers[1].views.simulateView({ id: videoIdServer1 }) | ||
38 | await servers[0].views.simulateView({ id: videoIdServer2 }) | ||
39 | await servers[1].views.simulateView({ id: videoIdServer2 }) | ||
40 | |||
41 | await waitJobs(servers) | ||
42 | |||
43 | sqlCommands = servers.map(s => new SQLCommand(s)) | ||
44 | }) | ||
45 | |||
46 | it('Should not clean old video views', async function () { | ||
47 | this.timeout(50000) | ||
48 | |||
49 | await killallServers([ servers[0] ]) | ||
50 | |||
51 | await servers[0].run({ views: { videos: { remote: { max_age: '10 days' } } } }) | ||
52 | |||
53 | await wait(6000) | ||
54 | |||
55 | // Should still have views | ||
56 | |||
57 | for (let i = 0; i < servers.length; i++) { | ||
58 | const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1) | ||
59 | expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views') | ||
60 | } | ||
61 | |||
62 | for (let i = 0; i < servers.length; i++) { | ||
63 | const total = await sqlCommands[i].countVideoViewsOf(videoIdServer2) | ||
64 | expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views') | ||
65 | } | ||
66 | }) | ||
67 | |||
68 | it('Should clean old video views', async function () { | ||
69 | this.timeout(50000) | ||
70 | |||
71 | await killallServers([ servers[0] ]) | ||
72 | |||
73 | await servers[0].run({ views: { videos: { remote: { max_age: '5 seconds' } } } }) | ||
74 | |||
75 | await wait(6000) | ||
76 | |||
77 | // Should still have views | ||
78 | |||
79 | for (let i = 0; i < servers.length; i++) { | ||
80 | const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1) | ||
81 | expect(total).to.equal(2) | ||
82 | } | ||
83 | |||
84 | const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer2) | ||
85 | expect(totalServer1).to.equal(0) | ||
86 | |||
87 | const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer2) | ||
88 | expect(totalServer2).to.equal(2) | ||
89 | }) | ||
90 | |||
91 | after(async function () { | ||
92 | for (const sqlCommand of sqlCommands) { | ||
93 | await sqlCommand.cleanup() | ||
94 | } | ||
95 | |||
96 | await cleanupTests(servers) | ||
97 | }) | ||
98 | }) | ||
diff --git a/packages/tests/src/cli/create-generate-storyboard-job.ts b/packages/tests/src/cli/create-generate-storyboard-job.ts new file mode 100644 index 000000000..5a1c61ef1 --- /dev/null +++ b/packages/tests/src/cli/create-generate-storyboard-job.ts | |||
@@ -0,0 +1,121 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { remove } from 'fs-extra/esm' | ||
5 | import { readdir } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createMultipleServers, | ||
11 | doubleFollow, | ||
12 | makeGetRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | waitJobs | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | import { SQLCommand } from '../shared/sql-command.js' | ||
18 | |||
19 | function listStoryboardFiles (server: PeerTubeServer) { | ||
20 | const storage = server.getDirectoryPath('storyboards') | ||
21 | |||
22 | return readdir(storage) | ||
23 | } | ||
24 | |||
25 | describe('Test create generate storyboard job', function () { | ||
26 | let servers: PeerTubeServer[] = [] | ||
27 | const uuids: string[] = [] | ||
28 | let sql: SQLCommand | ||
29 | let existingStoryboardName: string | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(120000) | ||
33 | |||
34 | // Run server 2 to have transcoding enabled | ||
35 | servers = await createMultipleServers(2) | ||
36 | await setAccessTokensToServers(servers) | ||
37 | |||
38 | await doubleFollow(servers[0], servers[1]) | ||
39 | |||
40 | for (let i = 0; i < 3; i++) { | ||
41 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video ' + i }) | ||
42 | uuids.push(uuid) | ||
43 | } | ||
44 | |||
45 | await waitJobs(servers) | ||
46 | |||
47 | const storage = servers[0].getDirectoryPath('storyboards') | ||
48 | for (const storyboard of await listStoryboardFiles(servers[0])) { | ||
49 | await remove(join(storage, storyboard)) | ||
50 | } | ||
51 | |||
52 | sql = new SQLCommand(servers[0]) | ||
53 | await sql.deleteAll('storyboard') | ||
54 | |||
55 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 4' }) | ||
56 | uuids.push(uuid) | ||
57 | |||
58 | await waitJobs(servers) | ||
59 | |||
60 | const storyboards = await listStoryboardFiles(servers[0]) | ||
61 | existingStoryboardName = storyboards[0] | ||
62 | }) | ||
63 | |||
64 | it('Should create a storyboard of a video', async function () { | ||
65 | this.timeout(120000) | ||
66 | |||
67 | for (const uuid of [ uuids[0], uuids[3] ]) { | ||
68 | const command = `npm run create-generate-storyboard-job -- -v ${uuid}` | ||
69 | await servers[0].cli.execWithEnv(command) | ||
70 | } | ||
71 | |||
72 | await waitJobs(servers) | ||
73 | |||
74 | { | ||
75 | const storyboards = await listStoryboardFiles(servers[0]) | ||
76 | expect(storyboards).to.have.lengthOf(2) | ||
77 | expect(storyboards).to.not.include(existingStoryboardName) | ||
78 | |||
79 | existingStoryboardName = storyboards[0] | ||
80 | } | ||
81 | |||
82 | for (const server of servers) { | ||
83 | for (const uuid of [ uuids[0], uuids[3] ]) { | ||
84 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
85 | expect(storyboards).to.have.lengthOf(1) | ||
86 | |||
87 | await makeGetRequest({ url: server.url, path: storyboards[0].storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
88 | } | ||
89 | } | ||
90 | }) | ||
91 | |||
92 | it('Should create missing storyboards', async function () { | ||
93 | this.timeout(120000) | ||
94 | |||
95 | const command = `npm run create-generate-storyboard-job -- -a` | ||
96 | await servers[0].cli.execWithEnv(command) | ||
97 | |||
98 | await waitJobs(servers) | ||
99 | |||
100 | { | ||
101 | const storyboards = await listStoryboardFiles(servers[0]) | ||
102 | expect(storyboards).to.have.lengthOf(4) | ||
103 | expect(storyboards).to.include(existingStoryboardName) | ||
104 | } | ||
105 | |||
106 | for (const server of servers) { | ||
107 | for (const uuid of uuids) { | ||
108 | const { storyboards } = await server.storyboard.list({ id: uuid }) | ||
109 | expect(storyboards).to.have.lengthOf(1) | ||
110 | |||
111 | await makeGetRequest({ url: server.url, path: storyboards[0].storyboardPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
112 | } | ||
113 | } | ||
114 | }) | ||
115 | |||
116 | after(async function () { | ||
117 | await sql.cleanup() | ||
118 | |||
119 | await cleanupTests(servers) | ||
120 | }) | ||
121 | }) | ||
diff --git a/packages/tests/src/cli/create-import-video-file-job.ts b/packages/tests/src/cli/create-import-video-file-job.ts new file mode 100644 index 000000000..fa934510c --- /dev/null +++ b/packages/tests/src/cli/create-import-video-file-job.ts | |||
@@ -0,0 +1,168 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { HttpStatusCode, VideoDetails, VideoFile, VideoInclude } from '@peertube/peertube-models' | ||
4 | import { areMockObjectStorageTestsDisabled, buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
5 | import { | ||
6 | ObjectStorageCommand, | ||
7 | PeerTubeServer, | ||
8 | cleanupTests, | ||
9 | createMultipleServers, | ||
10 | doubleFollow, | ||
11 | makeRawRequest, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { expect } from 'chai' | ||
16 | import { expectStartWith } from '../shared/checks.js' | ||
17 | |||
18 | function assertVideoProperties (video: VideoFile, resolution: number, extname: string, size?: number) { | ||
19 | expect(video).to.have.nested.property('resolution.id', resolution) | ||
20 | expect(video).to.have.property('torrentUrl').that.includes(`-${resolution}.torrent`) | ||
21 | expect(video).to.have.property('fileUrl').that.includes(`.${extname}`) | ||
22 | expect(video).to.have.property('magnetUri').that.includes(`.${extname}`) | ||
23 | expect(video).to.have.property('size').that.is.above(0) | ||
24 | |||
25 | if (size) expect(video.size).to.equal(size) | ||
26 | } | ||
27 | |||
28 | async function checkFiles (video: VideoDetails, objectStorage: ObjectStorageCommand) { | ||
29 | for (const file of video.files) { | ||
30 | if (objectStorage) expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
31 | |||
32 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
33 | } | ||
34 | } | ||
35 | |||
36 | function runTests (enableObjectStorage: boolean) { | ||
37 | let video1ShortId: string | ||
38 | let video2UUID: string | ||
39 | |||
40 | let servers: PeerTubeServer[] = [] | ||
41 | |||
42 | const objectStorage = new ObjectStorageCommand() | ||
43 | |||
44 | before(async function () { | ||
45 | this.timeout(90000) | ||
46 | |||
47 | const config = enableObjectStorage | ||
48 | ? objectStorage.getDefaultMockConfig() | ||
49 | : {} | ||
50 | |||
51 | // Run server 2 to have transcoding enabled | ||
52 | servers = await createMultipleServers(2, config) | ||
53 | await setAccessTokensToServers(servers) | ||
54 | |||
55 | await doubleFollow(servers[0], servers[1]) | ||
56 | |||
57 | if (enableObjectStorage) await objectStorage.prepareDefaultMockBuckets() | ||
58 | |||
59 | // Upload two videos for our needs | ||
60 | { | ||
61 | const { shortUUID } = await servers[0].videos.upload({ attributes: { name: 'video1' } }) | ||
62 | video1ShortId = shortUUID | ||
63 | } | ||
64 | |||
65 | { | ||
66 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video2' } }) | ||
67 | video2UUID = uuid | ||
68 | } | ||
69 | |||
70 | await waitJobs(servers) | ||
71 | |||
72 | for (const server of servers) { | ||
73 | await server.config.enableTranscoding() | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | it('Should run a import job on video 1 with a lower resolution', async function () { | ||
78 | const command = `npm run create-import-video-file-job -- -v ${video1ShortId} -i ${buildAbsoluteFixturePath('video_short_480.webm')}` | ||
79 | await servers[0].cli.execWithEnv(command) | ||
80 | |||
81 | await waitJobs(servers) | ||
82 | |||
83 | for (const server of servers) { | ||
84 | const { data: videos } = await server.videos.list() | ||
85 | expect(videos).to.have.lengthOf(2) | ||
86 | |||
87 | const video = videos.find(({ shortUUID }) => shortUUID === video1ShortId) | ||
88 | const videoDetails = await server.videos.get({ id: video.shortUUID }) | ||
89 | |||
90 | expect(videoDetails.files).to.have.lengthOf(2) | ||
91 | const [ originalVideo, transcodedVideo ] = videoDetails.files | ||
92 | assertVideoProperties(originalVideo, 720, 'webm', 218910) | ||
93 | assertVideoProperties(transcodedVideo, 480, 'webm', 69217) | ||
94 | |||
95 | await checkFiles(videoDetails, enableObjectStorage && objectStorage) | ||
96 | } | ||
97 | }) | ||
98 | |||
99 | it('Should run a import job on video 2 with the same resolution and a different extension', async function () { | ||
100 | const command = `npm run create-import-video-file-job -- -v ${video2UUID} -i ${buildAbsoluteFixturePath('video_short.ogv')}` | ||
101 | await servers[1].cli.execWithEnv(command) | ||
102 | |||
103 | await waitJobs(servers) | ||
104 | |||
105 | for (const server of servers) { | ||
106 | const { data: videos } = await server.videos.listWithToken({ include: VideoInclude.NOT_PUBLISHED_STATE }) | ||
107 | expect(videos).to.have.lengthOf(2) | ||
108 | |||
109 | const video = videos.find(({ uuid }) => uuid === video2UUID) | ||
110 | const videoDetails = await server.videos.get({ id: video.uuid }) | ||
111 | |||
112 | expect(videoDetails.files).to.have.lengthOf(4) | ||
113 | const [ originalVideo, transcodedVideo420, transcodedVideo320, transcodedVideo240 ] = videoDetails.files | ||
114 | assertVideoProperties(originalVideo, 720, 'ogv', 140849) | ||
115 | assertVideoProperties(transcodedVideo420, 480, 'mp4') | ||
116 | assertVideoProperties(transcodedVideo320, 360, 'mp4') | ||
117 | assertVideoProperties(transcodedVideo240, 240, 'mp4') | ||
118 | |||
119 | await checkFiles(videoDetails, enableObjectStorage && objectStorage) | ||
120 | } | ||
121 | }) | ||
122 | |||
123 | it('Should run a import job on video 2 with the same resolution and the same extension', async function () { | ||
124 | const command = `npm run create-import-video-file-job -- -v ${video1ShortId} -i ${buildAbsoluteFixturePath('video_short2.webm')}` | ||
125 | await servers[0].cli.execWithEnv(command) | ||
126 | |||
127 | await waitJobs(servers) | ||
128 | |||
129 | for (const server of servers) { | ||
130 | const { data: videos } = await server.videos.listWithToken({ include: VideoInclude.NOT_PUBLISHED_STATE }) | ||
131 | expect(videos).to.have.lengthOf(2) | ||
132 | |||
133 | const video = videos.find(({ shortUUID }) => shortUUID === video1ShortId) | ||
134 | const videoDetails = await server.videos.get({ id: video.uuid }) | ||
135 | |||
136 | expect(videoDetails.files).to.have.lengthOf(2) | ||
137 | const [ video720, video480 ] = videoDetails.files | ||
138 | assertVideoProperties(video720, 720, 'webm', 942961) | ||
139 | assertVideoProperties(video480, 480, 'webm', 69217) | ||
140 | |||
141 | await checkFiles(videoDetails, enableObjectStorage && objectStorage) | ||
142 | } | ||
143 | }) | ||
144 | |||
145 | it('Should not have run transcoding after an import job', async function () { | ||
146 | const { data } = await servers[0].jobs.list({ jobType: 'video-transcoding' }) | ||
147 | expect(data).to.have.lengthOf(0) | ||
148 | }) | ||
149 | |||
150 | after(async function () { | ||
151 | await objectStorage.cleanupMock() | ||
152 | |||
153 | await cleanupTests(servers) | ||
154 | }) | ||
155 | } | ||
156 | |||
157 | describe('Test create import video jobs', function () { | ||
158 | |||
159 | describe('On filesystem', function () { | ||
160 | runTests(false) | ||
161 | }) | ||
162 | |||
163 | describe('On object storage', function () { | ||
164 | if (areMockObjectStorageTestsDisabled()) return | ||
165 | |||
166 | runTests(true) | ||
167 | }) | ||
168 | }) | ||
diff --git a/packages/tests/src/cli/create-move-video-storage-job.ts b/packages/tests/src/cli/create-move-video-storage-job.ts new file mode 100644 index 000000000..1bee7414f --- /dev/null +++ b/packages/tests/src/cli/create-move-video-storage-job.ts | |||
@@ -0,0 +1,125 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { join } from 'path' | ||
4 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | makeRawRequest, | ||
11 | ObjectStorageCommand, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | import { expectStartWith } from '../shared/checks.js' | ||
17 | import { checkDirectoryIsEmpty } from '@tests/shared/directories.js' | ||
18 | |||
19 | async function checkFiles (origin: PeerTubeServer, video: VideoDetails, objectStorage?: ObjectStorageCommand) { | ||
20 | for (const file of video.files) { | ||
21 | const start = objectStorage | ||
22 | ? objectStorage.getMockWebVideosBaseUrl() | ||
23 | : origin.url | ||
24 | |||
25 | expectStartWith(file.fileUrl, start) | ||
26 | |||
27 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
28 | } | ||
29 | |||
30 | const start = objectStorage | ||
31 | ? objectStorage.getMockPlaylistBaseUrl() | ||
32 | : origin.url | ||
33 | |||
34 | const hls = video.streamingPlaylists[0] | ||
35 | expectStartWith(hls.playlistUrl, start) | ||
36 | expectStartWith(hls.segmentsSha256Url, start) | ||
37 | |||
38 | for (const file of hls.files) { | ||
39 | expectStartWith(file.fileUrl, start) | ||
40 | |||
41 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
42 | } | ||
43 | } | ||
44 | |||
45 | describe('Test create move video storage job', function () { | ||
46 | if (areMockObjectStorageTestsDisabled()) return | ||
47 | |||
48 | let servers: PeerTubeServer[] = [] | ||
49 | const uuids: string[] = [] | ||
50 | const objectStorage = new ObjectStorageCommand() | ||
51 | |||
52 | before(async function () { | ||
53 | this.timeout(360000) | ||
54 | |||
55 | // Run server 2 to have transcoding enabled | ||
56 | servers = await createMultipleServers(2) | ||
57 | await setAccessTokensToServers(servers) | ||
58 | |||
59 | await doubleFollow(servers[0], servers[1]) | ||
60 | |||
61 | await objectStorage.prepareDefaultMockBuckets() | ||
62 | |||
63 | await servers[0].config.enableTranscoding() | ||
64 | |||
65 | for (let i = 0; i < 3; i++) { | ||
66 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' + i } }) | ||
67 | uuids.push(uuid) | ||
68 | } | ||
69 | |||
70 | await waitJobs(servers) | ||
71 | |||
72 | await servers[0].kill() | ||
73 | await servers[0].run(objectStorage.getDefaultMockConfig()) | ||
74 | }) | ||
75 | |||
76 | it('Should move only one file', async function () { | ||
77 | this.timeout(120000) | ||
78 | |||
79 | const command = `npm run create-move-video-storage-job -- --to-object-storage -v ${uuids[1]}` | ||
80 | await servers[0].cli.execWithEnv(command, objectStorage.getDefaultMockConfig()) | ||
81 | await waitJobs(servers) | ||
82 | |||
83 | for (const server of servers) { | ||
84 | const video = await server.videos.get({ id: uuids[1] }) | ||
85 | |||
86 | await checkFiles(servers[0], video, objectStorage) | ||
87 | |||
88 | for (const id of [ uuids[0], uuids[2] ]) { | ||
89 | const video = await server.videos.get({ id }) | ||
90 | |||
91 | await checkFiles(servers[0], video) | ||
92 | } | ||
93 | } | ||
94 | }) | ||
95 | |||
96 | it('Should move all files', async function () { | ||
97 | this.timeout(120000) | ||
98 | |||
99 | const command = `npm run create-move-video-storage-job -- --to-object-storage --all-videos` | ||
100 | await servers[0].cli.execWithEnv(command, objectStorage.getDefaultMockConfig()) | ||
101 | await waitJobs(servers) | ||
102 | |||
103 | for (const server of servers) { | ||
104 | for (const id of [ uuids[0], uuids[2] ]) { | ||
105 | const video = await server.videos.get({ id }) | ||
106 | |||
107 | await checkFiles(servers[0], video, objectStorage) | ||
108 | } | ||
109 | } | ||
110 | }) | ||
111 | |||
112 | it('Should not have files on disk anymore', async function () { | ||
113 | await checkDirectoryIsEmpty(servers[0], 'web-videos', [ 'private' ]) | ||
114 | await checkDirectoryIsEmpty(servers[0], join('web-videos', 'private')) | ||
115 | |||
116 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls'), [ 'private' ]) | ||
117 | await checkDirectoryIsEmpty(servers[0], join('streaming-playlists', 'hls', 'private')) | ||
118 | }) | ||
119 | |||
120 | after(async function () { | ||
121 | await objectStorage.cleanupMock() | ||
122 | |||
123 | await cleanupTests(servers) | ||
124 | }) | ||
125 | }) | ||
diff --git a/packages/tests/src/cli/index.ts b/packages/tests/src/cli/index.ts new file mode 100644 index 000000000..94444ace3 --- /dev/null +++ b/packages/tests/src/cli/index.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | // Order of the tests we want to execute | ||
2 | import './create-import-video-file-job' | ||
3 | import './create-generate-storyboard-job' | ||
4 | import './create-move-video-storage-job' | ||
5 | import './peertube' | ||
6 | import './plugins' | ||
7 | import './prune-storage' | ||
8 | import './regenerate-thumbnails' | ||
9 | import './reset-password' | ||
10 | import './update-host' | ||
diff --git a/packages/tests/src/cli/peertube.ts b/packages/tests/src/cli/peertube.ts new file mode 100644 index 000000000..2c66b7a18 --- /dev/null +++ b/packages/tests/src/cli/peertube.ts | |||
@@ -0,0 +1,257 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | CLICommand, | ||
8 | createSingleServer, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | import { testHelloWorldRegisteredSettings } from '../shared/plugins.js' | ||
15 | |||
16 | describe('Test CLI wrapper', function () { | ||
17 | let server: PeerTubeServer | ||
18 | let userAccessToken: string | ||
19 | |||
20 | let cliCommand: CLICommand | ||
21 | |||
22 | const cmd = 'node ./apps/peertube-cli/dist/peertube.js' | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await createSingleServer(1, { | ||
28 | rates_limit: { | ||
29 | login: { | ||
30 | max: 30 | ||
31 | } | ||
32 | } | ||
33 | }) | ||
34 | await setAccessTokensToServers([ server ]) | ||
35 | |||
36 | await server.users.create({ username: 'user_1', password: 'super_password' }) | ||
37 | |||
38 | userAccessToken = await server.login.getAccessToken({ username: 'user_1', password: 'super_password' }) | ||
39 | |||
40 | { | ||
41 | const attributes = { name: 'user_channel', displayName: 'User channel', support: 'super support text' } | ||
42 | await server.channels.create({ token: userAccessToken, attributes }) | ||
43 | } | ||
44 | |||
45 | cliCommand = server.cli | ||
46 | }) | ||
47 | |||
48 | describe('Authentication and instance selection', function () { | ||
49 | |||
50 | it('Should get an access token', async function () { | ||
51 | const stdout = await cliCommand.execWithEnv(`${cmd} token --url ${server.url} --username user_1 --password super_password`) | ||
52 | const token = stdout.trim() | ||
53 | |||
54 | const body = await server.users.getMyInfo({ token }) | ||
55 | expect(body.username).to.equal('user_1') | ||
56 | }) | ||
57 | |||
58 | it('Should display no selected instance', async function () { | ||
59 | this.timeout(60000) | ||
60 | |||
61 | const stdout = await cliCommand.execWithEnv(`${cmd} --help`) | ||
62 | expect(stdout).to.contain('no instance selected') | ||
63 | }) | ||
64 | |||
65 | it('Should add a user', async function () { | ||
66 | this.timeout(60000) | ||
67 | |||
68 | await cliCommand.execWithEnv(`${cmd} auth add -u ${server.url} -U user_1 -p super_password`) | ||
69 | }) | ||
70 | |||
71 | it('Should not fail to add a user if there is a slash at the end of the instance URL', async function () { | ||
72 | this.timeout(60000) | ||
73 | |||
74 | let fullServerURL = server.url + '/' | ||
75 | |||
76 | await cliCommand.execWithEnv(`${cmd} auth add -u ${fullServerURL} -U user_1 -p super_password`) | ||
77 | |||
78 | fullServerURL = server.url + '/asdfasdf' | ||
79 | await cliCommand.execWithEnv(`${cmd} auth add -u ${fullServerURL} -U user_1 -p super_password`) | ||
80 | }) | ||
81 | |||
82 | it('Should default to this user', async function () { | ||
83 | this.timeout(60000) | ||
84 | |||
85 | const stdout = await cliCommand.execWithEnv(`${cmd} --help`) | ||
86 | expect(stdout).to.contain(`instance ${server.url} selected`) | ||
87 | }) | ||
88 | |||
89 | it('Should remember the user', async function () { | ||
90 | this.timeout(60000) | ||
91 | |||
92 | const stdout = await cliCommand.execWithEnv(`${cmd} auth list`) | ||
93 | expect(stdout).to.contain(server.url) | ||
94 | }) | ||
95 | }) | ||
96 | |||
97 | describe('Video upload', function () { | ||
98 | |||
99 | it('Should upload a video', async function () { | ||
100 | this.timeout(60000) | ||
101 | |||
102 | const fixture = buildAbsoluteFixturePath('60fps_720p_small.mp4') | ||
103 | const params = `-f ${fixture} --video-name 'test upload' --channel-name user_channel --support 'support_text'` | ||
104 | |||
105 | await cliCommand.execWithEnv(`${cmd} upload ${params}`) | ||
106 | }) | ||
107 | |||
108 | it('Should have the video uploaded', async function () { | ||
109 | const { total, data } = await server.videos.list() | ||
110 | expect(total).to.equal(1) | ||
111 | |||
112 | const video = await server.videos.get({ id: data[0].uuid }) | ||
113 | expect(video.name).to.equal('test upload') | ||
114 | expect(video.support).to.equal('support_text') | ||
115 | expect(video.channel.name).to.equal('user_channel') | ||
116 | }) | ||
117 | }) | ||
118 | |||
119 | describe('Admin auth', function () { | ||
120 | |||
121 | it('Should remove the auth user', async function () { | ||
122 | await cliCommand.execWithEnv(`${cmd} auth del ${server.url}`) | ||
123 | |||
124 | const stdout = await cliCommand.execWithEnv(`${cmd} --help`) | ||
125 | expect(stdout).to.contain('no instance selected') | ||
126 | }) | ||
127 | |||
128 | it('Should add the admin user', async function () { | ||
129 | await cliCommand.execWithEnv(`${cmd} auth add -u ${server.url} -U root -p test${server.internalServerNumber}`) | ||
130 | }) | ||
131 | }) | ||
132 | |||
133 | describe('Manage plugins', function () { | ||
134 | |||
135 | it('Should install a plugin', async function () { | ||
136 | this.timeout(60000) | ||
137 | |||
138 | await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world`) | ||
139 | }) | ||
140 | |||
141 | it('Should have registered settings', async function () { | ||
142 | await testHelloWorldRegisteredSettings(server) | ||
143 | }) | ||
144 | |||
145 | it('Should list installed plugins', async function () { | ||
146 | const res = await cliCommand.execWithEnv(`${cmd} plugins list`) | ||
147 | |||
148 | expect(res).to.contain('peertube-plugin-hello-world') | ||
149 | }) | ||
150 | |||
151 | it('Should uninstall the plugin', async function () { | ||
152 | const res = await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`) | ||
153 | |||
154 | expect(res).to.not.contain('peertube-plugin-hello-world') | ||
155 | }) | ||
156 | |||
157 | it('Should install a plugin in requested version', async function () { | ||
158 | this.timeout(60000) | ||
159 | |||
160 | await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world --plugin-version 0.0.17`) | ||
161 | }) | ||
162 | |||
163 | it('Should list installed plugins, in correct version', async function () { | ||
164 | const res = await cliCommand.execWithEnv(`${cmd} plugins list`) | ||
165 | |||
166 | expect(res).to.contain('peertube-plugin-hello-world') | ||
167 | expect(res).to.contain('0.0.17') | ||
168 | }) | ||
169 | |||
170 | it('Should uninstall the plugin again', async function () { | ||
171 | const res = await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`) | ||
172 | |||
173 | expect(res).to.not.contain('peertube-plugin-hello-world') | ||
174 | }) | ||
175 | |||
176 | it('Should install a plugin in requested beta version', async function () { | ||
177 | this.timeout(60000) | ||
178 | |||
179 | await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world --plugin-version 0.0.21-beta.1`) | ||
180 | |||
181 | const res = await cliCommand.execWithEnv(`${cmd} plugins list`) | ||
182 | |||
183 | expect(res).to.contain('peertube-plugin-hello-world') | ||
184 | expect(res).to.contain('0.0.21-beta.1') | ||
185 | |||
186 | await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`) | ||
187 | }) | ||
188 | }) | ||
189 | |||
190 | describe('Manage video redundancies', function () { | ||
191 | let anotherServer: PeerTubeServer | ||
192 | let video1Server2: number | ||
193 | let servers: PeerTubeServer[] | ||
194 | |||
195 | before(async function () { | ||
196 | this.timeout(120000) | ||
197 | |||
198 | anotherServer = await createSingleServer(2) | ||
199 | await setAccessTokensToServers([ anotherServer ]) | ||
200 | |||
201 | await doubleFollow(server, anotherServer) | ||
202 | |||
203 | servers = [ server, anotherServer ] | ||
204 | await waitJobs(servers) | ||
205 | |||
206 | const { uuid } = await anotherServer.videos.quickUpload({ name: 'super video' }) | ||
207 | await waitJobs(servers) | ||
208 | |||
209 | video1Server2 = await server.videos.getId({ uuid }) | ||
210 | }) | ||
211 | |||
212 | it('Should add a redundancy', async function () { | ||
213 | this.timeout(60000) | ||
214 | |||
215 | const params = `add --video ${video1Server2}` | ||
216 | await cliCommand.execWithEnv(`${cmd} redundancy ${params}`) | ||
217 | |||
218 | await waitJobs(servers) | ||
219 | }) | ||
220 | |||
221 | it('Should list redundancies', async function () { | ||
222 | this.timeout(60000) | ||
223 | |||
224 | { | ||
225 | const params = 'list-my-redundancies' | ||
226 | const stdout = await cliCommand.execWithEnv(`${cmd} redundancy ${params}`) | ||
227 | |||
228 | expect(stdout).to.contain('super video') | ||
229 | expect(stdout).to.contain(server.host) | ||
230 | } | ||
231 | }) | ||
232 | |||
233 | it('Should remove a redundancy', async function () { | ||
234 | this.timeout(60000) | ||
235 | |||
236 | const params = `remove --video ${video1Server2}` | ||
237 | await cliCommand.execWithEnv(`${cmd} redundancy ${params}`) | ||
238 | |||
239 | await waitJobs(servers) | ||
240 | |||
241 | { | ||
242 | const params = 'list-my-redundancies' | ||
243 | const stdout = await cliCommand.execWithEnv(`${cmd} redundancy ${params}`) | ||
244 | |||
245 | expect(stdout).to.not.contain('super video') | ||
246 | } | ||
247 | }) | ||
248 | |||
249 | after(async function () { | ||
250 | await cleanupTests([ anotherServer ]) | ||
251 | }) | ||
252 | }) | ||
253 | |||
254 | after(async function () { | ||
255 | await cleanupTests([ server ]) | ||
256 | }) | ||
257 | }) | ||
diff --git a/packages/tests/src/cli/plugins.ts b/packages/tests/src/cli/plugins.ts new file mode 100644 index 000000000..ab7f7dd85 --- /dev/null +++ b/packages/tests/src/cli/plugins.ts | |||
@@ -0,0 +1,76 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | killallServers, | ||
8 | PeerTubeServer, | ||
9 | PluginsCommand, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test plugin scripts', function () { | ||
14 | let server: PeerTubeServer | ||
15 | |||
16 | before(async function () { | ||
17 | this.timeout(30000) | ||
18 | |||
19 | server = await createSingleServer(1) | ||
20 | await setAccessTokensToServers([ server ]) | ||
21 | }) | ||
22 | |||
23 | it('Should install a plugin from stateless CLI', async function () { | ||
24 | this.timeout(60000) | ||
25 | |||
26 | const packagePath = PluginsCommand.getPluginTestPath() | ||
27 | |||
28 | await server.cli.execWithEnv(`npm run plugin:install -- --plugin-path ${packagePath}`) | ||
29 | }) | ||
30 | |||
31 | it('Should install a theme from stateless CLI', async function () { | ||
32 | this.timeout(60000) | ||
33 | |||
34 | await server.cli.execWithEnv(`npm run plugin:install -- --npm-name peertube-theme-background-red`) | ||
35 | }) | ||
36 | |||
37 | it('Should have the theme and the plugin registered when we restart peertube', async function () { | ||
38 | this.timeout(30000) | ||
39 | |||
40 | await killallServers([ server ]) | ||
41 | await server.run() | ||
42 | |||
43 | const config = await server.config.getConfig() | ||
44 | |||
45 | const plugin = config.plugin.registered | ||
46 | .find(p => p.name === 'test') | ||
47 | expect(plugin).to.not.be.undefined | ||
48 | |||
49 | const theme = config.theme.registered | ||
50 | .find(t => t.name === 'background-red') | ||
51 | expect(theme).to.not.be.undefined | ||
52 | }) | ||
53 | |||
54 | it('Should uninstall a plugin from stateless CLI', async function () { | ||
55 | this.timeout(60000) | ||
56 | |||
57 | await server.cli.execWithEnv(`npm run plugin:uninstall -- --npm-name peertube-plugin-test`) | ||
58 | }) | ||
59 | |||
60 | it('Should have removed the plugin on another peertube restart', async function () { | ||
61 | this.timeout(30000) | ||
62 | |||
63 | await killallServers([ server ]) | ||
64 | await server.run() | ||
65 | |||
66 | const config = await server.config.getConfig() | ||
67 | |||
68 | const plugin = config.plugin.registered | ||
69 | .find(p => p.name === 'test') | ||
70 | expect(plugin).to.be.undefined | ||
71 | }) | ||
72 | |||
73 | after(async function () { | ||
74 | await cleanupTests([ server ]) | ||
75 | }) | ||
76 | }) | ||
diff --git a/packages/tests/src/cli/prune-storage.ts b/packages/tests/src/cli/prune-storage.ts new file mode 100644 index 000000000..c07a2a975 --- /dev/null +++ b/packages/tests/src/cli/prune-storage.ts | |||
@@ -0,0 +1,224 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { createFile } from 'fs-extra/esm' | ||
5 | import { readdir } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { wait } from '@peertube/peertube-core-utils' | ||
8 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
9 | import { HttpStatusCode, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models' | ||
10 | import { | ||
11 | cleanupTests, | ||
12 | CLICommand, | ||
13 | createMultipleServers, | ||
14 | doubleFollow, | ||
15 | killallServers, | ||
16 | makeGetRequest, | ||
17 | PeerTubeServer, | ||
18 | setAccessTokensToServers, | ||
19 | setDefaultVideoChannel, | ||
20 | waitJobs | ||
21 | } from '@peertube/peertube-server-commands' | ||
22 | |||
23 | async function assertNotExists (server: PeerTubeServer, directory: string, substring: string) { | ||
24 | const files = await readdir(server.servers.buildDirectory(directory)) | ||
25 | |||
26 | for (const f of files) { | ||
27 | expect(f).to.not.contain(substring) | ||
28 | } | ||
29 | } | ||
30 | |||
31 | async function assertCountAreOkay (servers: PeerTubeServer[]) { | ||
32 | for (const server of servers) { | ||
33 | const videosCount = await server.servers.countFiles('web-videos') | ||
34 | expect(videosCount).to.equal(9) // 2 videos with 4 resolutions + private directory | ||
35 | |||
36 | const privateVideosCount = await server.servers.countFiles('web-videos/private') | ||
37 | expect(privateVideosCount).to.equal(4) | ||
38 | |||
39 | const torrentsCount = await server.servers.countFiles('torrents') | ||
40 | expect(torrentsCount).to.equal(24) | ||
41 | |||
42 | const previewsCount = await server.servers.countFiles('previews') | ||
43 | expect(previewsCount).to.equal(3) | ||
44 | |||
45 | const thumbnailsCount = await server.servers.countFiles('thumbnails') | ||
46 | expect(thumbnailsCount).to.equal(5) // 3 local videos, 1 local playlist, 2 remotes videos (lazy downloaded) and 1 remote playlist | ||
47 | |||
48 | const avatarsCount = await server.servers.countFiles('avatars') | ||
49 | expect(avatarsCount).to.equal(4) | ||
50 | |||
51 | const hlsRootCount = await server.servers.countFiles(join('streaming-playlists', 'hls')) | ||
52 | expect(hlsRootCount).to.equal(3) // 2 videos + private directory | ||
53 | |||
54 | const hlsPrivateRootCount = await server.servers.countFiles(join('streaming-playlists', 'hls', 'private')) | ||
55 | expect(hlsPrivateRootCount).to.equal(1) | ||
56 | } | ||
57 | } | ||
58 | |||
59 | describe('Test prune storage scripts', function () { | ||
60 | let servers: PeerTubeServer[] | ||
61 | const badNames: { [directory: string]: string[] } = {} | ||
62 | |||
63 | before(async function () { | ||
64 | this.timeout(120000) | ||
65 | |||
66 | servers = await createMultipleServers(2, { transcoding: { enabled: true } }) | ||
67 | await setAccessTokensToServers(servers) | ||
68 | await setDefaultVideoChannel(servers) | ||
69 | |||
70 | for (const server of servers) { | ||
71 | await server.videos.upload({ attributes: { name: 'video 1', privacy: VideoPrivacy.PUBLIC } }) | ||
72 | await server.videos.upload({ attributes: { name: 'video 2', privacy: VideoPrivacy.PUBLIC } }) | ||
73 | |||
74 | await server.videos.upload({ attributes: { name: 'video 3', privacy: VideoPrivacy.PRIVATE } }) | ||
75 | |||
76 | await server.users.updateMyAvatar({ fixture: 'avatar.png' }) | ||
77 | |||
78 | await server.playlists.create({ | ||
79 | attributes: { | ||
80 | displayName: 'playlist', | ||
81 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
82 | videoChannelId: server.store.channel.id, | ||
83 | thumbnailfile: 'custom-thumbnail.jpg' | ||
84 | } | ||
85 | }) | ||
86 | } | ||
87 | |||
88 | await doubleFollow(servers[0], servers[1]) | ||
89 | |||
90 | // Lazy load the remote avatars | ||
91 | { | ||
92 | const account = await servers[0].accounts.get({ accountName: 'root@' + servers[1].host }) | ||
93 | |||
94 | for (const avatar of account.avatars) { | ||
95 | await makeGetRequest({ | ||
96 | url: servers[0].url, | ||
97 | path: avatar.path, | ||
98 | expectedStatus: HttpStatusCode.OK_200 | ||
99 | }) | ||
100 | } | ||
101 | } | ||
102 | |||
103 | { | ||
104 | const account = await servers[1].accounts.get({ accountName: 'root@' + servers[0].host }) | ||
105 | for (const avatar of account.avatars) { | ||
106 | await makeGetRequest({ | ||
107 | url: servers[1].url, | ||
108 | path: avatar.path, | ||
109 | expectedStatus: HttpStatusCode.OK_200 | ||
110 | }) | ||
111 | } | ||
112 | } | ||
113 | |||
114 | await wait(1000) | ||
115 | |||
116 | await waitJobs(servers) | ||
117 | await killallServers(servers) | ||
118 | |||
119 | await wait(1000) | ||
120 | }) | ||
121 | |||
122 | it('Should have the files on the disk', async function () { | ||
123 | await assertCountAreOkay(servers) | ||
124 | }) | ||
125 | |||
126 | it('Should create some dirty files', async function () { | ||
127 | for (let i = 0; i < 2; i++) { | ||
128 | { | ||
129 | const basePublic = servers[0].servers.buildDirectory('web-videos') | ||
130 | const basePrivate = servers[0].servers.buildDirectory(join('web-videos', 'private')) | ||
131 | |||
132 | const n1 = buildUUID() + '.mp4' | ||
133 | const n2 = buildUUID() + '.webm' | ||
134 | |||
135 | await createFile(join(basePublic, n1)) | ||
136 | await createFile(join(basePublic, n2)) | ||
137 | await createFile(join(basePrivate, n1)) | ||
138 | await createFile(join(basePrivate, n2)) | ||
139 | |||
140 | badNames['web-videos'] = [ n1, n2 ] | ||
141 | } | ||
142 | |||
143 | { | ||
144 | const base = servers[0].servers.buildDirectory('torrents') | ||
145 | |||
146 | const n1 = buildUUID() + '-240.torrent' | ||
147 | const n2 = buildUUID() + '-480.torrent' | ||
148 | |||
149 | await createFile(join(base, n1)) | ||
150 | await createFile(join(base, n2)) | ||
151 | |||
152 | badNames['torrents'] = [ n1, n2 ] | ||
153 | } | ||
154 | |||
155 | { | ||
156 | const base = servers[0].servers.buildDirectory('thumbnails') | ||
157 | |||
158 | const n1 = buildUUID() + '.jpg' | ||
159 | const n2 = buildUUID() + '.jpg' | ||
160 | |||
161 | await createFile(join(base, n1)) | ||
162 | await createFile(join(base, n2)) | ||
163 | |||
164 | badNames['thumbnails'] = [ n1, n2 ] | ||
165 | } | ||
166 | |||
167 | { | ||
168 | const base = servers[0].servers.buildDirectory('previews') | ||
169 | |||
170 | const n1 = buildUUID() + '.jpg' | ||
171 | const n2 = buildUUID() + '.jpg' | ||
172 | |||
173 | await createFile(join(base, n1)) | ||
174 | await createFile(join(base, n2)) | ||
175 | |||
176 | badNames['previews'] = [ n1, n2 ] | ||
177 | } | ||
178 | |||
179 | { | ||
180 | const base = servers[0].servers.buildDirectory('avatars') | ||
181 | |||
182 | const n1 = buildUUID() + '.png' | ||
183 | const n2 = buildUUID() + '.jpg' | ||
184 | |||
185 | await createFile(join(base, n1)) | ||
186 | await createFile(join(base, n2)) | ||
187 | |||
188 | badNames['avatars'] = [ n1, n2 ] | ||
189 | } | ||
190 | |||
191 | { | ||
192 | const directory = join('streaming-playlists', 'hls') | ||
193 | const basePublic = servers[0].servers.buildDirectory(directory) | ||
194 | const basePrivate = servers[0].servers.buildDirectory(join(directory, 'private')) | ||
195 | |||
196 | const n1 = buildUUID() | ||
197 | await createFile(join(basePublic, n1)) | ||
198 | await createFile(join(basePrivate, n1)) | ||
199 | badNames[directory] = [ n1 ] | ||
200 | } | ||
201 | } | ||
202 | }) | ||
203 | |||
204 | it('Should run prune storage', async function () { | ||
205 | this.timeout(30000) | ||
206 | |||
207 | const env = servers[0].cli.getEnv() | ||
208 | await CLICommand.exec(`echo y | ${env} npm run prune-storage`) | ||
209 | }) | ||
210 | |||
211 | it('Should have removed files', async function () { | ||
212 | await assertCountAreOkay(servers) | ||
213 | |||
214 | for (const directory of Object.keys(badNames)) { | ||
215 | for (const name of badNames[directory]) { | ||
216 | await assertNotExists(servers[0], directory, name) | ||
217 | } | ||
218 | } | ||
219 | }) | ||
220 | |||
221 | after(async function () { | ||
222 | await cleanupTests(servers) | ||
223 | }) | ||
224 | }) | ||
diff --git a/packages/tests/src/cli/regenerate-thumbnails.ts b/packages/tests/src/cli/regenerate-thumbnails.ts new file mode 100644 index 000000000..1448e5cfc --- /dev/null +++ b/packages/tests/src/cli/regenerate-thumbnails.ts | |||
@@ -0,0 +1,122 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { writeFile } from 'fs/promises' | ||
3 | import { basename, join } from 'path' | ||
4 | import { HttpStatusCode, Video } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | makeGetRequest, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | async function testThumbnail (server: PeerTubeServer, videoId: number | string) { | ||
16 | const video = await server.videos.get({ id: videoId }) | ||
17 | |||
18 | const requests = [ | ||
19 | makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }), | ||
20 | makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
21 | ] | ||
22 | |||
23 | for (const req of requests) { | ||
24 | const res = await req | ||
25 | expect(res.body).to.not.have.lengthOf(0) | ||
26 | } | ||
27 | } | ||
28 | |||
29 | describe('Test regenerate thumbnails script', function () { | ||
30 | let servers: PeerTubeServer[] | ||
31 | |||
32 | let video1: Video | ||
33 | let video2: Video | ||
34 | let remoteVideo: Video | ||
35 | |||
36 | let thumbnail1Path: string | ||
37 | let thumbnailRemotePath: string | ||
38 | |||
39 | before(async function () { | ||
40 | this.timeout(60000) | ||
41 | |||
42 | servers = await createMultipleServers(2) | ||
43 | await setAccessTokensToServers(servers) | ||
44 | |||
45 | await doubleFollow(servers[0], servers[1]) | ||
46 | |||
47 | { | ||
48 | const videoUUID1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid | ||
49 | video1 = await servers[0].videos.get({ id: videoUUID1 }) | ||
50 | |||
51 | thumbnail1Path = join(servers[0].servers.buildDirectory('thumbnails'), basename(video1.thumbnailPath)) | ||
52 | |||
53 | const videoUUID2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).uuid | ||
54 | video2 = await servers[0].videos.get({ id: videoUUID2 }) | ||
55 | } | ||
56 | |||
57 | { | ||
58 | const videoUUID = (await servers[1].videos.quickUpload({ name: 'video 3' })).uuid | ||
59 | await waitJobs(servers) | ||
60 | |||
61 | remoteVideo = await servers[0].videos.get({ id: videoUUID }) | ||
62 | |||
63 | // Load remote thumbnail on disk | ||
64 | await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
65 | |||
66 | thumbnailRemotePath = join(servers[0].servers.buildDirectory('thumbnails'), basename(remoteVideo.thumbnailPath)) | ||
67 | } | ||
68 | |||
69 | await writeFile(thumbnail1Path, '') | ||
70 | await writeFile(thumbnailRemotePath, '') | ||
71 | }) | ||
72 | |||
73 | it('Should have empty thumbnails', async function () { | ||
74 | { | ||
75 | const res = await makeGetRequest({ url: servers[0].url, path: video1.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
76 | expect(res.body).to.have.lengthOf(0) | ||
77 | } | ||
78 | |||
79 | { | ||
80 | const res = await makeGetRequest({ url: servers[0].url, path: video2.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
81 | expect(res.body).to.not.have.lengthOf(0) | ||
82 | } | ||
83 | |||
84 | { | ||
85 | const res = await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
86 | expect(res.body).to.have.lengthOf(0) | ||
87 | } | ||
88 | }) | ||
89 | |||
90 | it('Should regenerate local thumbnails from the CLI', async function () { | ||
91 | this.timeout(15000) | ||
92 | |||
93 | await servers[0].cli.execWithEnv(`npm run regenerate-thumbnails`) | ||
94 | }) | ||
95 | |||
96 | it('Should have generated new thumbnail files', async function () { | ||
97 | await testThumbnail(servers[0], video1.uuid) | ||
98 | await testThumbnail(servers[0], video2.uuid) | ||
99 | |||
100 | const res = await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
101 | expect(res.body).to.have.lengthOf(0) | ||
102 | }) | ||
103 | |||
104 | it('Should have deleted old thumbnail files', async function () { | ||
105 | { | ||
106 | await makeGetRequest({ url: servers[0].url, path: video1.thumbnailPath, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
107 | } | ||
108 | |||
109 | { | ||
110 | await makeGetRequest({ url: servers[0].url, path: video2.thumbnailPath, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | ||
111 | } | ||
112 | |||
113 | { | ||
114 | const res = await makeGetRequest({ url: servers[0].url, path: remoteVideo.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
115 | expect(res.body).to.have.lengthOf(0) | ||
116 | } | ||
117 | }) | ||
118 | |||
119 | after(async function () { | ||
120 | await cleanupTests(servers) | ||
121 | }) | ||
122 | }) | ||
diff --git a/packages/tests/src/cli/reset-password.ts b/packages/tests/src/cli/reset-password.ts new file mode 100644 index 000000000..62e1a37a0 --- /dev/null +++ b/packages/tests/src/cli/reset-password.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import { cleanupTests, CLICommand, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
2 | |||
3 | describe('Test reset password scripts', function () { | ||
4 | let server: PeerTubeServer | ||
5 | |||
6 | before(async function () { | ||
7 | this.timeout(30000) | ||
8 | server = await createSingleServer(1) | ||
9 | await setAccessTokensToServers([ server ]) | ||
10 | |||
11 | await server.users.create({ username: 'user_1', password: 'super password' }) | ||
12 | }) | ||
13 | |||
14 | it('Should change the user password from CLI', async function () { | ||
15 | this.timeout(60000) | ||
16 | |||
17 | const env = server.cli.getEnv() | ||
18 | await CLICommand.exec(`echo coucou | ${env} npm run reset-password -- -u user_1`) | ||
19 | |||
20 | await server.login.login({ user: { username: 'user_1', password: 'coucou' } }) | ||
21 | }) | ||
22 | |||
23 | after(async function () { | ||
24 | await cleanupTests([ server ]) | ||
25 | }) | ||
26 | }) | ||
diff --git a/packages/tests/src/cli/update-host.ts b/packages/tests/src/cli/update-host.ts new file mode 100644 index 000000000..e5f165e5e --- /dev/null +++ b/packages/tests/src/cli/update-host.ts | |||
@@ -0,0 +1,134 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { getAllFiles } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | killallServers, | ||
9 | makeActivityPubGetRequest, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | import { parseTorrentVideo } from '@tests/shared/webtorrent.js' | ||
15 | |||
16 | describe('Test update host scripts', function () { | ||
17 | let server: PeerTubeServer | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(60000) | ||
21 | |||
22 | const overrideConfig = { | ||
23 | webserver: { | ||
24 | port: 9256 | ||
25 | } | ||
26 | } | ||
27 | // Run server 2 to have transcoding enabled | ||
28 | server = await createSingleServer(2, overrideConfig) | ||
29 | await setAccessTokensToServers([ server ]) | ||
30 | |||
31 | // Upload two videos for our needs | ||
32 | const { uuid: video1UUID } = await server.videos.upload() | ||
33 | await server.videos.upload() | ||
34 | |||
35 | // Create a user | ||
36 | await server.users.create({ username: 'toto', password: 'coucou' }) | ||
37 | |||
38 | // Create channel | ||
39 | const videoChannel = { | ||
40 | name: 'second_channel', | ||
41 | displayName: 'second video channel', | ||
42 | description: 'super video channel description' | ||
43 | } | ||
44 | await server.channels.create({ attributes: videoChannel }) | ||
45 | |||
46 | // Create comments | ||
47 | const text = 'my super first comment' | ||
48 | await server.comments.createThread({ videoId: video1UUID, text }) | ||
49 | |||
50 | await waitJobs(server) | ||
51 | }) | ||
52 | |||
53 | it('Should run update host', async function () { | ||
54 | this.timeout(30000) | ||
55 | |||
56 | await killallServers([ server ]) | ||
57 | // Run server with standard configuration | ||
58 | await server.run() | ||
59 | |||
60 | await server.cli.execWithEnv(`npm run update-host`) | ||
61 | }) | ||
62 | |||
63 | it('Should have updated videos url', async function () { | ||
64 | const { total, data } = await server.videos.list() | ||
65 | expect(total).to.equal(2) | ||
66 | |||
67 | for (const video of data) { | ||
68 | const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid) | ||
69 | |||
70 | expect(body.id).to.equal('http://127.0.0.1:9002/videos/watch/' + video.uuid) | ||
71 | |||
72 | const videoDetails = await server.videos.get({ id: video.uuid }) | ||
73 | |||
74 | expect(videoDetails.trackerUrls[0]).to.include(server.host) | ||
75 | expect(videoDetails.streamingPlaylists[0].playlistUrl).to.include(server.host) | ||
76 | expect(videoDetails.streamingPlaylists[0].segmentsSha256Url).to.include(server.host) | ||
77 | } | ||
78 | }) | ||
79 | |||
80 | it('Should have updated video channels url', async function () { | ||
81 | const { data, total } = await server.channels.list({ sort: '-name' }) | ||
82 | expect(total).to.equal(3) | ||
83 | |||
84 | for (const channel of data) { | ||
85 | const { body } = await makeActivityPubGetRequest(server.url, '/video-channels/' + channel.name) | ||
86 | |||
87 | expect(body.id).to.equal('http://127.0.0.1:9002/video-channels/' + channel.name) | ||
88 | } | ||
89 | }) | ||
90 | |||
91 | it('Should have updated accounts url', async function () { | ||
92 | const body = await server.accounts.list() | ||
93 | expect(body.total).to.equal(3) | ||
94 | |||
95 | for (const account of body.data) { | ||
96 | const usernameWithDomain = account.name | ||
97 | const { body } = await makeActivityPubGetRequest(server.url, '/accounts/' + usernameWithDomain) | ||
98 | |||
99 | expect(body.id).to.equal('http://127.0.0.1:9002/accounts/' + usernameWithDomain) | ||
100 | } | ||
101 | }) | ||
102 | |||
103 | it('Should have updated torrent hosts', async function () { | ||
104 | this.timeout(30000) | ||
105 | |||
106 | const { data } = await server.videos.list() | ||
107 | expect(data).to.have.lengthOf(2) | ||
108 | |||
109 | for (const video of data) { | ||
110 | const videoDetails = await server.videos.get({ id: video.id }) | ||
111 | const files = getAllFiles(videoDetails) | ||
112 | |||
113 | expect(files).to.have.lengthOf(8) | ||
114 | |||
115 | for (const file of files) { | ||
116 | expect(file.magnetUri).to.contain('127.0.0.1%3A9002%2Ftracker%2Fsocket') | ||
117 | expect(file.magnetUri).to.contain('127.0.0.1%3A9002%2Fstatic%2F') | ||
118 | |||
119 | const torrent = await parseTorrentVideo(server, file) | ||
120 | const announceWS = torrent.announce.find(a => a === 'ws://127.0.0.1:9002/tracker/socket') | ||
121 | expect(announceWS).to.not.be.undefined | ||
122 | |||
123 | const announceHttp = torrent.announce.find(a => a === 'http://127.0.0.1:9002/tracker/announce') | ||
124 | expect(announceHttp).to.not.be.undefined | ||
125 | |||
126 | expect(torrent.urlList[0]).to.contain('http://127.0.0.1:9002/static/') | ||
127 | } | ||
128 | } | ||
129 | }) | ||
130 | |||
131 | after(async function () { | ||
132 | await cleanupTests([ server ]) | ||
133 | }) | ||
134 | }) | ||
diff --git a/packages/tests/src/client.ts b/packages/tests/src/client.ts new file mode 100644 index 000000000..a16205494 --- /dev/null +++ b/packages/tests/src/client.ts | |||
@@ -0,0 +1,556 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { omit } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | Account, | ||
7 | HTMLServerConfig, | ||
8 | HttpStatusCode, | ||
9 | ServerConfig, | ||
10 | VideoPlaylistCreateResult, | ||
11 | VideoPlaylistPrivacy, | ||
12 | VideoPrivacy | ||
13 | } from '@peertube/peertube-models' | ||
14 | import { | ||
15 | cleanupTests, | ||
16 | createMultipleServers, | ||
17 | doubleFollow, | ||
18 | makeGetRequest, | ||
19 | makeHTMLRequest, | ||
20 | PeerTubeServer, | ||
21 | setAccessTokensToServers, | ||
22 | setDefaultVideoChannel, | ||
23 | waitJobs | ||
24 | } from '@peertube/peertube-server-commands' | ||
25 | |||
26 | function checkIndexTags (html: string, title: string, description: string, css: string, config: ServerConfig) { | ||
27 | expect(html).to.contain('<title>' + title + '</title>') | ||
28 | expect(html).to.contain('<meta name="description" content="' + description + '" />') | ||
29 | expect(html).to.contain('<style class="custom-css-style">' + css + '</style>') | ||
30 | |||
31 | const htmlConfig: HTMLServerConfig = omit(config, [ 'signup' ]) | ||
32 | const configObjectString = JSON.stringify(htmlConfig) | ||
33 | const configEscapedString = JSON.stringify(configObjectString) | ||
34 | |||
35 | expect(html).to.contain(`<script type="application/javascript">window.PeerTubeServerConfig = ${configEscapedString}</script>`) | ||
36 | } | ||
37 | |||
38 | describe('Test a client controllers', function () { | ||
39 | let servers: PeerTubeServer[] = [] | ||
40 | let account: Account | ||
41 | |||
42 | const videoName = 'my super name for server 1' | ||
43 | const videoDescription = 'my<br> super __description__ for *server* 1<p></p>' | ||
44 | const videoDescriptionPlainText = 'my super description for server 1' | ||
45 | |||
46 | const playlistName = 'super playlist name' | ||
47 | const playlistDescription = 'super playlist description' | ||
48 | let playlist: VideoPlaylistCreateResult | ||
49 | |||
50 | const channelDescription = 'my super channel description' | ||
51 | |||
52 | const watchVideoBasePaths = [ '/videos/watch/', '/w/' ] | ||
53 | const watchPlaylistBasePaths = [ '/videos/watch/playlist/', '/w/p/' ] | ||
54 | |||
55 | let videoIds: (string | number)[] = [] | ||
56 | let privateVideoId: string | ||
57 | let internalVideoId: string | ||
58 | let unlistedVideoId: string | ||
59 | let passwordProtectedVideoId: string | ||
60 | |||
61 | let playlistIds: (string | number)[] = [] | ||
62 | |||
63 | before(async function () { | ||
64 | this.timeout(120000) | ||
65 | |||
66 | servers = await createMultipleServers(2) | ||
67 | |||
68 | await setAccessTokensToServers(servers) | ||
69 | |||
70 | await doubleFollow(servers[0], servers[1]) | ||
71 | |||
72 | await setDefaultVideoChannel(servers) | ||
73 | |||
74 | await servers[0].channels.update({ | ||
75 | channelName: servers[0].store.channel.name, | ||
76 | attributes: { description: channelDescription } | ||
77 | }) | ||
78 | |||
79 | // Public video | ||
80 | |||
81 | { | ||
82 | const attributes = { name: videoName, description: videoDescription } | ||
83 | await servers[0].videos.upload({ attributes }) | ||
84 | |||
85 | const { data } = await servers[0].videos.list() | ||
86 | expect(data.length).to.equal(1) | ||
87 | |||
88 | const video = data[0] | ||
89 | servers[0].store.video = video | ||
90 | videoIds = [ video.id, video.uuid, video.shortUUID ] | ||
91 | } | ||
92 | |||
93 | { | ||
94 | ({ uuid: privateVideoId } = await servers[0].videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })); | ||
95 | ({ uuid: unlistedVideoId } = await servers[0].videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED })); | ||
96 | ({ uuid: internalVideoId } = await servers[0].videos.quickUpload({ name: 'internal', privacy: VideoPrivacy.INTERNAL })); | ||
97 | ({ uuid: passwordProtectedVideoId } = await servers[0].videos.quickUpload({ | ||
98 | name: 'password protected', | ||
99 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
100 | videoPasswords: [ 'password' ] | ||
101 | })) | ||
102 | } | ||
103 | |||
104 | // Playlist | ||
105 | |||
106 | { | ||
107 | const attributes = { | ||
108 | displayName: playlistName, | ||
109 | description: playlistDescription, | ||
110 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
111 | videoChannelId: servers[0].store.channel.id | ||
112 | } | ||
113 | |||
114 | playlist = await servers[0].playlists.create({ attributes }) | ||
115 | playlistIds = [ playlist.id, playlist.shortUUID, playlist.uuid ] | ||
116 | |||
117 | await servers[0].playlists.addElement({ playlistId: playlist.shortUUID, attributes: { videoId: servers[0].store.video.id } }) | ||
118 | } | ||
119 | |||
120 | // Account | ||
121 | |||
122 | { | ||
123 | await servers[0].users.updateMe({ description: 'my account description' }) | ||
124 | |||
125 | account = await servers[0].accounts.get({ accountName: `${servers[0].store.user.username}@${servers[0].host}` }) | ||
126 | } | ||
127 | |||
128 | await waitJobs(servers) | ||
129 | }) | ||
130 | |||
131 | describe('oEmbed', function () { | ||
132 | |||
133 | it('Should have valid oEmbed discovery tags for videos', async function () { | ||
134 | for (const basePath of watchVideoBasePaths) { | ||
135 | for (const id of videoIds) { | ||
136 | const res = await makeGetRequest({ | ||
137 | url: servers[0].url, | ||
138 | path: basePath + id, | ||
139 | accept: 'text/html', | ||
140 | expectedStatus: HttpStatusCode.OK_200 | ||
141 | }) | ||
142 | |||
143 | const expectedLink = `<link rel="alternate" type="application/json+oembed" href="${servers[0].url}/services/oembed?` + | ||
144 | `url=http%3A%2F%2F${servers[0].hostname}%3A${servers[0].port}%2Fw%2F${servers[0].store.video.shortUUID}" ` + | ||
145 | `title="${servers[0].store.video.name}" />` | ||
146 | |||
147 | expect(res.text).to.contain(expectedLink) | ||
148 | } | ||
149 | } | ||
150 | }) | ||
151 | |||
152 | it('Should have valid oEmbed discovery tags for a playlist', async function () { | ||
153 | for (const basePath of watchPlaylistBasePaths) { | ||
154 | for (const id of playlistIds) { | ||
155 | const res = await makeGetRequest({ | ||
156 | url: servers[0].url, | ||
157 | path: basePath + id, | ||
158 | accept: 'text/html', | ||
159 | expectedStatus: HttpStatusCode.OK_200 | ||
160 | }) | ||
161 | |||
162 | const expectedLink = `<link rel="alternate" type="application/json+oembed" href="${servers[0].url}/services/oembed?` + | ||
163 | `url=http%3A%2F%2F${servers[0].hostname}%3A${servers[0].port}%2Fw%2Fp%2F${playlist.shortUUID}" ` + | ||
164 | `title="${playlistName}" />` | ||
165 | |||
166 | expect(res.text).to.contain(expectedLink) | ||
167 | } | ||
168 | } | ||
169 | }) | ||
170 | }) | ||
171 | |||
172 | describe('Open Graph', function () { | ||
173 | |||
174 | async function accountPageTest (path: string) { | ||
175 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
176 | const text = res.text | ||
177 | |||
178 | expect(text).to.contain(`<meta property="og:title" content="${account.displayName}" />`) | ||
179 | expect(text).to.contain(`<meta property="og:description" content="${account.description}" />`) | ||
180 | expect(text).to.contain('<meta property="og:type" content="website" />') | ||
181 | expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/a/${servers[0].store.user.username}" />`) | ||
182 | } | ||
183 | |||
184 | async function channelPageTest (path: string) { | ||
185 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
186 | const text = res.text | ||
187 | |||
188 | expect(text).to.contain(`<meta property="og:title" content="${servers[0].store.channel.displayName}" />`) | ||
189 | expect(text).to.contain(`<meta property="og:description" content="${channelDescription}" />`) | ||
190 | expect(text).to.contain('<meta property="og:type" content="website" />') | ||
191 | expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/c/${servers[0].store.channel.name}" />`) | ||
192 | } | ||
193 | |||
194 | async function watchVideoPageTest (path: string) { | ||
195 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
196 | const text = res.text | ||
197 | |||
198 | expect(text).to.contain(`<meta property="og:title" content="${videoName}" />`) | ||
199 | expect(text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`) | ||
200 | expect(text).to.contain('<meta property="og:type" content="video" />') | ||
201 | expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/${servers[0].store.video.shortUUID}" />`) | ||
202 | } | ||
203 | |||
204 | async function watchPlaylistPageTest (path: string) { | ||
205 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
206 | const text = res.text | ||
207 | |||
208 | expect(text).to.contain(`<meta property="og:title" content="${playlistName}" />`) | ||
209 | expect(text).to.contain(`<meta property="og:description" content="${playlistDescription}" />`) | ||
210 | expect(text).to.contain('<meta property="og:type" content="video" />') | ||
211 | expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/p/${playlist.shortUUID}" />`) | ||
212 | } | ||
213 | |||
214 | it('Should have valid Open Graph tags on the account page', async function () { | ||
215 | await accountPageTest('/accounts/' + servers[0].store.user.username) | ||
216 | await accountPageTest('/a/' + servers[0].store.user.username) | ||
217 | await accountPageTest('/@' + servers[0].store.user.username) | ||
218 | }) | ||
219 | |||
220 | it('Should have valid Open Graph tags on the channel page', async function () { | ||
221 | await channelPageTest('/video-channels/' + servers[0].store.channel.name) | ||
222 | await channelPageTest('/c/' + servers[0].store.channel.name) | ||
223 | await channelPageTest('/@' + servers[0].store.channel.name) | ||
224 | }) | ||
225 | |||
226 | it('Should have valid Open Graph tags on the watch page', async function () { | ||
227 | for (const path of watchVideoBasePaths) { | ||
228 | for (const id of videoIds) { | ||
229 | await watchVideoPageTest(path + id) | ||
230 | } | ||
231 | } | ||
232 | }) | ||
233 | |||
234 | it('Should have valid Open Graph tags on the watch page with thread id Angular param', async function () { | ||
235 | for (const path of watchVideoBasePaths) { | ||
236 | for (const id of videoIds) { | ||
237 | await watchVideoPageTest(path + id + ';threadId=1') | ||
238 | } | ||
239 | } | ||
240 | }) | ||
241 | |||
242 | it('Should have valid Open Graph tags on the watch playlist page', async function () { | ||
243 | for (const path of watchPlaylistBasePaths) { | ||
244 | for (const id of playlistIds) { | ||
245 | await watchPlaylistPageTest(path + id) | ||
246 | } | ||
247 | } | ||
248 | }) | ||
249 | }) | ||
250 | |||
251 | describe('Twitter card', async function () { | ||
252 | |||
253 | describe('Not whitelisted', function () { | ||
254 | |||
255 | async function accountPageTest (path: string) { | ||
256 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
257 | const text = res.text | ||
258 | |||
259 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') | ||
260 | expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | ||
261 | expect(text).to.contain(`<meta property="twitter:title" content="${account.name}" />`) | ||
262 | expect(text).to.contain(`<meta property="twitter:description" content="${account.description}" />`) | ||
263 | } | ||
264 | |||
265 | async function channelPageTest (path: string) { | ||
266 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
267 | const text = res.text | ||
268 | |||
269 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') | ||
270 | expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | ||
271 | expect(text).to.contain(`<meta property="twitter:title" content="${servers[0].store.channel.displayName}" />`) | ||
272 | expect(text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`) | ||
273 | } | ||
274 | |||
275 | async function watchVideoPageTest (path: string) { | ||
276 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
277 | const text = res.text | ||
278 | |||
279 | expect(text).to.contain('<meta property="twitter:card" content="summary_large_image" />') | ||
280 | expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | ||
281 | expect(text).to.contain(`<meta property="twitter:title" content="${videoName}" />`) | ||
282 | expect(text).to.contain(`<meta property="twitter:description" content="${videoDescriptionPlainText}" />`) | ||
283 | } | ||
284 | |||
285 | async function watchPlaylistPageTest (path: string) { | ||
286 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
287 | const text = res.text | ||
288 | |||
289 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') | ||
290 | expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | ||
291 | expect(text).to.contain(`<meta property="twitter:title" content="${playlistName}" />`) | ||
292 | expect(text).to.contain(`<meta property="twitter:description" content="${playlistDescription}" />`) | ||
293 | } | ||
294 | |||
295 | it('Should have valid twitter card on the watch video page', async function () { | ||
296 | for (const path of watchVideoBasePaths) { | ||
297 | for (const id of videoIds) { | ||
298 | await watchVideoPageTest(path + id) | ||
299 | } | ||
300 | } | ||
301 | }) | ||
302 | |||
303 | it('Should have valid twitter card on the watch playlist page', async function () { | ||
304 | for (const path of watchPlaylistBasePaths) { | ||
305 | for (const id of playlistIds) { | ||
306 | await watchPlaylistPageTest(path + id) | ||
307 | } | ||
308 | } | ||
309 | }) | ||
310 | |||
311 | it('Should have valid twitter card on the account page', async function () { | ||
312 | await accountPageTest('/accounts/' + account.name) | ||
313 | await accountPageTest('/a/' + account.name) | ||
314 | await accountPageTest('/@' + account.name) | ||
315 | }) | ||
316 | |||
317 | it('Should have valid twitter card on the channel page', async function () { | ||
318 | await channelPageTest('/video-channels/' + servers[0].store.channel.name) | ||
319 | await channelPageTest('/c/' + servers[0].store.channel.name) | ||
320 | await channelPageTest('/@' + servers[0].store.channel.name) | ||
321 | }) | ||
322 | }) | ||
323 | |||
324 | describe('Whitelisted', function () { | ||
325 | |||
326 | before(async function () { | ||
327 | const config = await servers[0].config.getCustomConfig() | ||
328 | config.services.twitter = { | ||
329 | username: '@Kuja', | ||
330 | whitelisted: true | ||
331 | } | ||
332 | |||
333 | await servers[0].config.updateCustomConfig({ newCustomConfig: config }) | ||
334 | }) | ||
335 | |||
336 | async function accountPageTest (path: string) { | ||
337 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
338 | const text = res.text | ||
339 | |||
340 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') | ||
341 | expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') | ||
342 | } | ||
343 | |||
344 | async function channelPageTest (path: string) { | ||
345 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
346 | const text = res.text | ||
347 | |||
348 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') | ||
349 | expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') | ||
350 | } | ||
351 | |||
352 | async function watchVideoPageTest (path: string) { | ||
353 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
354 | const text = res.text | ||
355 | |||
356 | expect(text).to.contain('<meta property="twitter:card" content="player" />') | ||
357 | expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') | ||
358 | } | ||
359 | |||
360 | async function watchPlaylistPageTest (path: string) { | ||
361 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 }) | ||
362 | const text = res.text | ||
363 | |||
364 | expect(text).to.contain('<meta property="twitter:card" content="player" />') | ||
365 | expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') | ||
366 | } | ||
367 | |||
368 | it('Should have valid twitter card on the watch video page', async function () { | ||
369 | for (const path of watchVideoBasePaths) { | ||
370 | for (const id of videoIds) { | ||
371 | await watchVideoPageTest(path + id) | ||
372 | } | ||
373 | } | ||
374 | }) | ||
375 | |||
376 | it('Should have valid twitter card on the watch playlist page', async function () { | ||
377 | for (const path of watchPlaylistBasePaths) { | ||
378 | for (const id of playlistIds) { | ||
379 | await watchPlaylistPageTest(path + id) | ||
380 | } | ||
381 | } | ||
382 | }) | ||
383 | |||
384 | it('Should have valid twitter card on the account page', async function () { | ||
385 | await accountPageTest('/accounts/' + account.name) | ||
386 | await accountPageTest('/a/' + account.name) | ||
387 | await accountPageTest('/@' + account.name) | ||
388 | }) | ||
389 | |||
390 | it('Should have valid twitter card on the channel page', async function () { | ||
391 | await channelPageTest('/video-channels/' + servers[0].store.channel.name) | ||
392 | await channelPageTest('/c/' + servers[0].store.channel.name) | ||
393 | await channelPageTest('/@' + servers[0].store.channel.name) | ||
394 | }) | ||
395 | }) | ||
396 | }) | ||
397 | |||
398 | describe('Index HTML', function () { | ||
399 | |||
400 | it('Should have valid index html tags (title, description...)', async function () { | ||
401 | const config = await servers[0].config.getConfig() | ||
402 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') | ||
403 | |||
404 | const description = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.' | ||
405 | checkIndexTags(res.text, 'PeerTube', description, '', config) | ||
406 | }) | ||
407 | |||
408 | it('Should update the customized configuration and have the correct index html tags', async function () { | ||
409 | await servers[0].config.updateCustomSubConfig({ | ||
410 | newConfig: { | ||
411 | instance: { | ||
412 | name: 'PeerTube updated', | ||
413 | shortDescription: 'my short description', | ||
414 | description: 'my super description', | ||
415 | terms: 'my super terms', | ||
416 | defaultNSFWPolicy: 'blur', | ||
417 | defaultClientRoute: '/videos/recently-added', | ||
418 | customizations: { | ||
419 | javascript: 'alert("coucou")', | ||
420 | css: 'body { background-color: red; }' | ||
421 | } | ||
422 | } | ||
423 | } | ||
424 | }) | ||
425 | |||
426 | const config = await servers[0].config.getConfig() | ||
427 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') | ||
428 | |||
429 | checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config) | ||
430 | }) | ||
431 | |||
432 | it('Should have valid index html updated tags (title, description...)', async function () { | ||
433 | const config = await servers[0].config.getConfig() | ||
434 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') | ||
435 | |||
436 | checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config) | ||
437 | }) | ||
438 | |||
439 | it('Should use the original video URL for the canonical tag', async function () { | ||
440 | for (const basePath of watchVideoBasePaths) { | ||
441 | for (const id of videoIds) { | ||
442 | const res = await makeHTMLRequest(servers[1].url, basePath + id) | ||
443 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/videos/watch/${servers[0].store.video.uuid}" />`) | ||
444 | } | ||
445 | } | ||
446 | }) | ||
447 | |||
448 | it('Should use the original account URL for the canonical tag', async function () { | ||
449 | const accountURLtest = res => { | ||
450 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/accounts/root" />`) | ||
451 | } | ||
452 | |||
453 | accountURLtest(await makeHTMLRequest(servers[1].url, '/accounts/root@' + servers[0].host)) | ||
454 | accountURLtest(await makeHTMLRequest(servers[1].url, '/a/root@' + servers[0].host)) | ||
455 | accountURLtest(await makeHTMLRequest(servers[1].url, '/@root@' + servers[0].host)) | ||
456 | }) | ||
457 | |||
458 | it('Should use the original channel URL for the canonical tag', async function () { | ||
459 | const channelURLtests = res => { | ||
460 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-channels/root_channel" />`) | ||
461 | } | ||
462 | |||
463 | channelURLtests(await makeHTMLRequest(servers[1].url, '/video-channels/root_channel@' + servers[0].host)) | ||
464 | channelURLtests(await makeHTMLRequest(servers[1].url, '/c/root_channel@' + servers[0].host)) | ||
465 | channelURLtests(await makeHTMLRequest(servers[1].url, '/@root_channel@' + servers[0].host)) | ||
466 | }) | ||
467 | |||
468 | it('Should use the original playlist URL for the canonical tag', async function () { | ||
469 | for (const basePath of watchPlaylistBasePaths) { | ||
470 | for (const id of playlistIds) { | ||
471 | const res = await makeHTMLRequest(servers[1].url, basePath + id) | ||
472 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-playlists/${playlist.uuid}" />`) | ||
473 | } | ||
474 | } | ||
475 | }) | ||
476 | |||
477 | it('Should add noindex meta tag for remote accounts', async function () { | ||
478 | const handle = 'root@' + servers[0].host | ||
479 | const paths = [ '/accounts/', '/a/', '/@' ] | ||
480 | |||
481 | for (const path of paths) { | ||
482 | { | ||
483 | const { text } = await makeHTMLRequest(servers[1].url, path + handle) | ||
484 | expect(text).to.contain('<meta name="robots" content="noindex" />') | ||
485 | } | ||
486 | |||
487 | { | ||
488 | const { text } = await makeHTMLRequest(servers[0].url, path + handle) | ||
489 | expect(text).to.not.contain('<meta name="robots" content="noindex" />') | ||
490 | } | ||
491 | } | ||
492 | }) | ||
493 | |||
494 | it('Should add noindex meta tag for remote channels', async function () { | ||
495 | const handle = 'root_channel@' + servers[0].host | ||
496 | const paths = [ '/video-channels/', '/c/', '/@' ] | ||
497 | |||
498 | for (const path of paths) { | ||
499 | { | ||
500 | const { text } = await makeHTMLRequest(servers[1].url, path + handle) | ||
501 | expect(text).to.contain('<meta name="robots" content="noindex" />') | ||
502 | } | ||
503 | |||
504 | { | ||
505 | const { text } = await makeHTMLRequest(servers[0].url, path + handle) | ||
506 | expect(text).to.not.contain('<meta name="robots" content="noindex" />') | ||
507 | } | ||
508 | } | ||
509 | }) | ||
510 | |||
511 | it('Should not display internal/private/password protected video', async function () { | ||
512 | for (const basePath of watchVideoBasePaths) { | ||
513 | for (const id of [ privateVideoId, internalVideoId, passwordProtectedVideoId ]) { | ||
514 | const res = await makeGetRequest({ | ||
515 | url: servers[0].url, | ||
516 | path: basePath + id, | ||
517 | accept: 'text/html', | ||
518 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
519 | }) | ||
520 | |||
521 | expect(res.text).to.not.contain('internal') | ||
522 | expect(res.text).to.not.contain('private') | ||
523 | expect(res.text).to.not.contain('password protected') | ||
524 | } | ||
525 | } | ||
526 | }) | ||
527 | |||
528 | it('Should add noindex meta tag for unlisted video', async function () { | ||
529 | for (const basePath of watchVideoBasePaths) { | ||
530 | const res = await makeGetRequest({ | ||
531 | url: servers[0].url, | ||
532 | path: basePath + unlistedVideoId, | ||
533 | accept: 'text/html', | ||
534 | expectedStatus: HttpStatusCode.OK_200 | ||
535 | }) | ||
536 | |||
537 | expect(res.text).to.contain('unlisted') | ||
538 | expect(res.text).to.contain('<meta name="robots" content="noindex" />') | ||
539 | } | ||
540 | }) | ||
541 | }) | ||
542 | |||
543 | describe('Embed HTML', function () { | ||
544 | |||
545 | it('Should have the correct embed html tags', async function () { | ||
546 | const config = await servers[0].config.getConfig() | ||
547 | const res = await makeHTMLRequest(servers[0].url, servers[0].store.video.embedPath) | ||
548 | |||
549 | checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config) | ||
550 | }) | ||
551 | }) | ||
552 | |||
553 | after(async function () { | ||
554 | await cleanupTests(servers) | ||
555 | }) | ||
556 | }) | ||
diff --git a/packages/tests/src/external-plugins/akismet.ts b/packages/tests/src/external-plugins/akismet.ts new file mode 100644 index 000000000..c6d3b7752 --- /dev/null +++ b/packages/tests/src/external-plugins/akismet.ts | |||
@@ -0,0 +1,160 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Official plugin Akismet', function () { | ||
15 | let servers: PeerTubeServer[] | ||
16 | let videoUUID: string | ||
17 | |||
18 | before(async function () { | ||
19 | this.timeout(30000) | ||
20 | |||
21 | servers = await createMultipleServers(2) | ||
22 | await setAccessTokensToServers(servers) | ||
23 | |||
24 | await servers[0].plugins.install({ | ||
25 | npmName: 'peertube-plugin-akismet' | ||
26 | }) | ||
27 | |||
28 | if (!process.env.AKISMET_KEY) throw new Error('Missing AKISMET_KEY from env') | ||
29 | |||
30 | await servers[0].plugins.updateSettings({ | ||
31 | npmName: 'peertube-plugin-akismet', | ||
32 | settings: { | ||
33 | 'akismet-api-key': process.env.AKISMET_KEY | ||
34 | } | ||
35 | }) | ||
36 | |||
37 | await doubleFollow(servers[0], servers[1]) | ||
38 | }) | ||
39 | |||
40 | describe('Local threads/replies', function () { | ||
41 | |||
42 | before(async function () { | ||
43 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' }) | ||
44 | videoUUID = uuid | ||
45 | }) | ||
46 | |||
47 | it('Should not detect a thread as spam', async function () { | ||
48 | await servers[0].comments.createThread({ videoId: videoUUID, text: 'comment' }) | ||
49 | }) | ||
50 | |||
51 | it('Should not detect a reply as spam', async function () { | ||
52 | await servers[0].comments.addReplyToLastThread({ text: 'reply' }) | ||
53 | }) | ||
54 | |||
55 | it('Should detect a thread as spam', async function () { | ||
56 | await servers[0].comments.createThread({ | ||
57 | videoId: videoUUID, | ||
58 | text: 'akismet-guaranteed-spam', | ||
59 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
60 | }) | ||
61 | }) | ||
62 | |||
63 | it('Should detect a thread as spam', async function () { | ||
64 | await servers[0].comments.createThread({ videoId: videoUUID, text: 'comment' }) | ||
65 | await servers[0].comments.addReplyToLastThread({ text: 'akismet-guaranteed-spam', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
66 | }) | ||
67 | }) | ||
68 | |||
69 | describe('Remote threads/replies', function () { | ||
70 | |||
71 | before(async function () { | ||
72 | this.timeout(60000) | ||
73 | |||
74 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' }) | ||
75 | videoUUID = uuid | ||
76 | |||
77 | await waitJobs(servers) | ||
78 | }) | ||
79 | |||
80 | it('Should not detect a thread as spam', async function () { | ||
81 | this.timeout(30000) | ||
82 | |||
83 | await servers[1].comments.createThread({ videoId: videoUUID, text: 'remote comment 1' }) | ||
84 | await waitJobs(servers) | ||
85 | |||
86 | const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) | ||
87 | expect(data).to.have.lengthOf(1) | ||
88 | }) | ||
89 | |||
90 | it('Should not detect a reply as spam', async function () { | ||
91 | this.timeout(30000) | ||
92 | |||
93 | await servers[1].comments.addReplyToLastThread({ text: 'I agree with you' }) | ||
94 | await waitJobs(servers) | ||
95 | |||
96 | const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) | ||
97 | expect(data).to.have.lengthOf(1) | ||
98 | |||
99 | const tree = await servers[0].comments.getThread({ videoId: videoUUID, threadId: data[0].id }) | ||
100 | expect(tree.children).to.have.lengthOf(1) | ||
101 | }) | ||
102 | |||
103 | it('Should detect a thread as spam', async function () { | ||
104 | this.timeout(30000) | ||
105 | |||
106 | await servers[1].comments.createThread({ videoId: videoUUID, text: 'akismet-guaranteed-spam' }) | ||
107 | await waitJobs(servers) | ||
108 | |||
109 | const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) | ||
110 | expect(data).to.have.lengthOf(1) | ||
111 | }) | ||
112 | |||
113 | it('Should detect a thread as spam', async function () { | ||
114 | this.timeout(30000) | ||
115 | |||
116 | await servers[1].comments.addReplyToLastThread({ text: 'akismet-guaranteed-spam' }) | ||
117 | await waitJobs(servers) | ||
118 | |||
119 | const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) | ||
120 | expect(data).to.have.lengthOf(1) | ||
121 | |||
122 | const thread = data[0] | ||
123 | const tree = await servers[0].comments.getThread({ videoId: videoUUID, threadId: thread.id }) | ||
124 | expect(tree.children).to.have.lengthOf(1) | ||
125 | }) | ||
126 | }) | ||
127 | |||
128 | describe('Signup', function () { | ||
129 | |||
130 | before(async function () { | ||
131 | await servers[0].config.updateExistingSubConfig({ | ||
132 | newConfig: { | ||
133 | signup: { | ||
134 | enabled: true | ||
135 | } | ||
136 | } | ||
137 | }) | ||
138 | }) | ||
139 | |||
140 | it('Should allow signup', async function () { | ||
141 | await servers[0].registrations.register({ | ||
142 | username: 'user1', | ||
143 | displayName: 'user 1' | ||
144 | }) | ||
145 | }) | ||
146 | |||
147 | it('Should detect a signup as SPAM', async function () { | ||
148 | await servers[0].registrations.register({ | ||
149 | username: 'user2', | ||
150 | displayName: 'user 2', | ||
151 | email: 'akismet-guaranteed-spam@example.com', | ||
152 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
153 | }) | ||
154 | }) | ||
155 | }) | ||
156 | |||
157 | after(async function () { | ||
158 | await cleanupTests(servers) | ||
159 | }) | ||
160 | }) | ||
diff --git a/packages/tests/src/external-plugins/auth-ldap.ts b/packages/tests/src/external-plugins/auth-ldap.ts new file mode 100644 index 000000000..ad058110c --- /dev/null +++ b/packages/tests/src/external-plugins/auth-ldap.ts | |||
@@ -0,0 +1,117 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | |||
7 | describe('Official plugin auth-ldap', function () { | ||
8 | let server: PeerTubeServer | ||
9 | let accessToken: string | ||
10 | let userId: number | ||
11 | |||
12 | before(async function () { | ||
13 | this.timeout(30000) | ||
14 | |||
15 | server = await createSingleServer(1) | ||
16 | await setAccessTokensToServers([ server ]) | ||
17 | |||
18 | await server.plugins.install({ npmName: 'peertube-plugin-auth-ldap' }) | ||
19 | }) | ||
20 | |||
21 | it('Should not login with without LDAP settings', async function () { | ||
22 | await server.login.login({ user: { username: 'fry', password: 'fry' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
23 | }) | ||
24 | |||
25 | it('Should not login with bad LDAP settings', async function () { | ||
26 | await server.plugins.updateSettings({ | ||
27 | npmName: 'peertube-plugin-auth-ldap', | ||
28 | settings: { | ||
29 | 'bind-credentials': 'GoodNewsEveryone', | ||
30 | 'bind-dn': 'cn=admin,dc=planetexpress,dc=com', | ||
31 | 'insecure-tls': false, | ||
32 | 'mail-property': 'mail', | ||
33 | 'search-base': 'ou=people,dc=planetexpress,dc=com', | ||
34 | 'search-filter': '(|(mail={{username}})(uid={{username}}))', | ||
35 | 'url': 'ldap://127.0.0.1:390', | ||
36 | 'username-property': 'uid' | ||
37 | } | ||
38 | }) | ||
39 | |||
40 | await server.login.login({ user: { username: 'fry', password: 'fry' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
41 | }) | ||
42 | |||
43 | it('Should not login with good LDAP settings but wrong username/password', async function () { | ||
44 | await server.plugins.updateSettings({ | ||
45 | npmName: 'peertube-plugin-auth-ldap', | ||
46 | settings: { | ||
47 | 'bind-credentials': 'GoodNewsEveryone', | ||
48 | 'bind-dn': 'cn=admin,dc=planetexpress,dc=com', | ||
49 | 'insecure-tls': false, | ||
50 | 'mail-property': 'mail', | ||
51 | 'search-base': 'ou=people,dc=planetexpress,dc=com', | ||
52 | 'search-filter': '(|(mail={{username}})(uid={{username}}))', | ||
53 | 'url': 'ldap://127.0.0.1:10389', | ||
54 | 'username-property': 'uid' | ||
55 | } | ||
56 | }) | ||
57 | |||
58 | await server.login.login({ user: { username: 'fry', password: 'bad password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
59 | await server.login.login({ user: { username: 'fryr', password: 'fry' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
60 | }) | ||
61 | |||
62 | it('Should login with the appropriate username/password', async function () { | ||
63 | accessToken = await server.login.getAccessToken({ username: 'fry', password: 'fry' }) | ||
64 | }) | ||
65 | |||
66 | it('Should login with the appropriate email/password', async function () { | ||
67 | accessToken = await server.login.getAccessToken({ username: 'fry@planetexpress.com', password: 'fry' }) | ||
68 | }) | ||
69 | |||
70 | it('Should login get my profile', async function () { | ||
71 | const body = await server.users.getMyInfo({ token: accessToken }) | ||
72 | expect(body.username).to.equal('fry') | ||
73 | expect(body.email).to.equal('fry@planetexpress.com') | ||
74 | |||
75 | userId = body.id | ||
76 | }) | ||
77 | |||
78 | it('Should upload a video', async function () { | ||
79 | await server.videos.upload({ token: accessToken, attributes: { name: 'my super video' } }) | ||
80 | }) | ||
81 | |||
82 | it('Should not be able to login if the user is banned', async function () { | ||
83 | await server.users.banUser({ userId }) | ||
84 | |||
85 | await server.login.login({ | ||
86 | user: { username: 'fry@planetexpress.com', password: 'fry' }, | ||
87 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
88 | }) | ||
89 | }) | ||
90 | |||
91 | it('Should be able to login if the user is unbanned', async function () { | ||
92 | await server.users.unbanUser({ userId }) | ||
93 | |||
94 | await server.login.login({ user: { username: 'fry@planetexpress.com', password: 'fry' } }) | ||
95 | }) | ||
96 | |||
97 | it('Should not be able to ask password reset', async function () { | ||
98 | await server.users.askResetPassword({ email: 'fry@planetexpress.com', expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
99 | }) | ||
100 | |||
101 | it('Should not be able to ask email verification', async function () { | ||
102 | await server.users.askSendVerifyEmail({ email: 'fry@planetexpress.com', expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
103 | }) | ||
104 | |||
105 | it('Should not login if the plugin is uninstalled', async function () { | ||
106 | await server.plugins.uninstall({ npmName: 'peertube-plugin-auth-ldap' }) | ||
107 | |||
108 | await server.login.login({ | ||
109 | user: { username: 'fry@planetexpress.com', password: 'fry' }, | ||
110 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
111 | }) | ||
112 | }) | ||
113 | |||
114 | after(async function () { | ||
115 | await cleanupTests([ server ]) | ||
116 | }) | ||
117 | }) | ||
diff --git a/packages/tests/src/external-plugins/auto-block-videos.ts b/packages/tests/src/external-plugins/auto-block-videos.ts new file mode 100644 index 000000000..6146c827c --- /dev/null +++ b/packages/tests/src/external-plugins/auto-block-videos.ts | |||
@@ -0,0 +1,167 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { Video } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | import { MockBlocklist } from '../shared/mock-servers/index.js' | ||
15 | |||
16 | async function check (server: PeerTubeServer, videoUUID: string, exists = true) { | ||
17 | const { data } = await server.videos.list() | ||
18 | |||
19 | const video = data.find(v => v.uuid === videoUUID) | ||
20 | |||
21 | if (exists) expect(video).to.not.be.undefined | ||
22 | else expect(video).to.be.undefined | ||
23 | } | ||
24 | |||
25 | describe('Official plugin auto-block videos', function () { | ||
26 | let servers: PeerTubeServer[] | ||
27 | let blocklistServer: MockBlocklist | ||
28 | let server1Videos: Video[] = [] | ||
29 | let server2Videos: Video[] = [] | ||
30 | let port: number | ||
31 | |||
32 | before(async function () { | ||
33 | this.timeout(120000) | ||
34 | |||
35 | servers = await createMultipleServers(2) | ||
36 | await setAccessTokensToServers(servers) | ||
37 | |||
38 | for (const server of servers) { | ||
39 | await server.plugins.install({ npmName: 'peertube-plugin-auto-block-videos' }) | ||
40 | } | ||
41 | |||
42 | blocklistServer = new MockBlocklist() | ||
43 | port = await blocklistServer.initialize() | ||
44 | |||
45 | await servers[0].videos.quickUpload({ name: 'video server 1' }) | ||
46 | await servers[1].videos.quickUpload({ name: 'video server 2' }) | ||
47 | await servers[1].videos.quickUpload({ name: 'video 2 server 2' }) | ||
48 | await servers[1].videos.quickUpload({ name: 'video 3 server 2' }) | ||
49 | |||
50 | { | ||
51 | const { data } = await servers[0].videos.list() | ||
52 | server1Videos = data.map(v => Object.assign(v, { url: servers[0].url + '/videos/watch/' + v.uuid })) | ||
53 | } | ||
54 | |||
55 | { | ||
56 | const { data } = await servers[1].videos.list() | ||
57 | server2Videos = data.map(v => Object.assign(v, { url: servers[1].url + '/videos/watch/' + v.uuid })) | ||
58 | } | ||
59 | |||
60 | await doubleFollow(servers[0], servers[1]) | ||
61 | }) | ||
62 | |||
63 | it('Should update plugin settings', async function () { | ||
64 | await servers[0].plugins.updateSettings({ | ||
65 | npmName: 'peertube-plugin-auto-block-videos', | ||
66 | settings: { | ||
67 | 'blocklist-urls': `http://127.0.0.1:${port}/blocklist`, | ||
68 | 'check-seconds-interval': 1 | ||
69 | } | ||
70 | }) | ||
71 | }) | ||
72 | |||
73 | it('Should auto block a video', async function () { | ||
74 | await check(servers[0], server2Videos[0].uuid, true) | ||
75 | |||
76 | blocklistServer.replace({ | ||
77 | data: [ | ||
78 | { | ||
79 | value: server2Videos[0].url | ||
80 | } | ||
81 | ] | ||
82 | }) | ||
83 | |||
84 | await wait(2000) | ||
85 | |||
86 | await check(servers[0], server2Videos[0].uuid, false) | ||
87 | }) | ||
88 | |||
89 | it('Should have video in blacklists', async function () { | ||
90 | const body = await servers[0].blacklist.list() | ||
91 | |||
92 | const videoBlacklists = body.data | ||
93 | expect(videoBlacklists).to.have.lengthOf(1) | ||
94 | expect(videoBlacklists[0].reason).to.contains('Automatically blocked from auto block plugin') | ||
95 | expect(videoBlacklists[0].video.name).to.equal(server2Videos[0].name) | ||
96 | }) | ||
97 | |||
98 | it('Should not block a local video', async function () { | ||
99 | await check(servers[0], server1Videos[0].uuid, true) | ||
100 | |||
101 | blocklistServer.replace({ | ||
102 | data: [ | ||
103 | { | ||
104 | value: server1Videos[0].url | ||
105 | } | ||
106 | ] | ||
107 | }) | ||
108 | |||
109 | await wait(2000) | ||
110 | |||
111 | await check(servers[0], server1Videos[0].uuid, true) | ||
112 | }) | ||
113 | |||
114 | it('Should remove a video block', async function () { | ||
115 | await check(servers[0], server2Videos[0].uuid, false) | ||
116 | |||
117 | blocklistServer.replace({ | ||
118 | data: [ | ||
119 | { | ||
120 | value: server2Videos[0].url, | ||
121 | action: 'remove' | ||
122 | } | ||
123 | ] | ||
124 | }) | ||
125 | |||
126 | await wait(2000) | ||
127 | |||
128 | await check(servers[0], server2Videos[0].uuid, true) | ||
129 | }) | ||
130 | |||
131 | it('Should auto block a video, manually unblock it and do not reblock it automatically', async function () { | ||
132 | this.timeout(20000) | ||
133 | |||
134 | const video = server2Videos[1] | ||
135 | |||
136 | await check(servers[0], video.uuid, true) | ||
137 | |||
138 | blocklistServer.replace({ | ||
139 | data: [ | ||
140 | { | ||
141 | value: video.url, | ||
142 | updatedAt: new Date().toISOString() | ||
143 | } | ||
144 | ] | ||
145 | }) | ||
146 | |||
147 | await wait(2000) | ||
148 | |||
149 | await check(servers[0], video.uuid, false) | ||
150 | |||
151 | await servers[0].blacklist.remove({ videoId: video.uuid }) | ||
152 | |||
153 | await check(servers[0], video.uuid, true) | ||
154 | |||
155 | await killallServers([ servers[0] ]) | ||
156 | await servers[0].run() | ||
157 | await wait(2000) | ||
158 | |||
159 | await check(servers[0], video.uuid, true) | ||
160 | }) | ||
161 | |||
162 | after(async function () { | ||
163 | await blocklistServer.terminate() | ||
164 | |||
165 | await cleanupTests(servers) | ||
166 | }) | ||
167 | }) | ||
diff --git a/packages/tests/src/external-plugins/auto-mute.ts b/packages/tests/src/external-plugins/auto-mute.ts new file mode 100644 index 000000000..b4050e236 --- /dev/null +++ b/packages/tests/src/external-plugins/auto-mute.ts | |||
@@ -0,0 +1,216 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | killallServers, | ||
11 | makeGetRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { MockBlocklist } from '../shared/mock-servers/index.js' | ||
16 | |||
17 | describe('Official plugin auto-mute', function () { | ||
18 | const autoMuteListPath = '/plugins/auto-mute/router/api/v1/mute-list' | ||
19 | let servers: PeerTubeServer[] | ||
20 | let blocklistServer: MockBlocklist | ||
21 | let port: number | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(120000) | ||
25 | |||
26 | servers = await createMultipleServers(2) | ||
27 | await setAccessTokensToServers(servers) | ||
28 | |||
29 | for (const server of servers) { | ||
30 | await server.plugins.install({ npmName: 'peertube-plugin-auto-mute' }) | ||
31 | } | ||
32 | |||
33 | blocklistServer = new MockBlocklist() | ||
34 | port = await blocklistServer.initialize() | ||
35 | |||
36 | await servers[0].videos.quickUpload({ name: 'video server 1' }) | ||
37 | await servers[1].videos.quickUpload({ name: 'video server 2' }) | ||
38 | |||
39 | await doubleFollow(servers[0], servers[1]) | ||
40 | }) | ||
41 | |||
42 | it('Should update plugin settings', async function () { | ||
43 | await servers[0].plugins.updateSettings({ | ||
44 | npmName: 'peertube-plugin-auto-mute', | ||
45 | settings: { | ||
46 | 'blocklist-urls': `http://127.0.0.1:${port}/blocklist`, | ||
47 | 'check-seconds-interval': 1 | ||
48 | } | ||
49 | }) | ||
50 | }) | ||
51 | |||
52 | it('Should add a server blocklist', async function () { | ||
53 | blocklistServer.replace({ | ||
54 | data: [ | ||
55 | { | ||
56 | value: servers[1].host | ||
57 | } | ||
58 | ] | ||
59 | }) | ||
60 | |||
61 | await wait(2000) | ||
62 | |||
63 | const { total } = await servers[0].videos.list() | ||
64 | expect(total).to.equal(1) | ||
65 | }) | ||
66 | |||
67 | it('Should remove a server blocklist', async function () { | ||
68 | blocklistServer.replace({ | ||
69 | data: [ | ||
70 | { | ||
71 | value: servers[1].host, | ||
72 | action: 'remove' | ||
73 | } | ||
74 | ] | ||
75 | }) | ||
76 | |||
77 | await wait(2000) | ||
78 | |||
79 | const { total } = await servers[0].videos.list() | ||
80 | expect(total).to.equal(2) | ||
81 | }) | ||
82 | |||
83 | it('Should add an account blocklist', async function () { | ||
84 | blocklistServer.replace({ | ||
85 | data: [ | ||
86 | { | ||
87 | value: 'root@' + servers[1].host | ||
88 | } | ||
89 | ] | ||
90 | }) | ||
91 | |||
92 | await wait(2000) | ||
93 | |||
94 | const { total } = await servers[0].videos.list() | ||
95 | expect(total).to.equal(1) | ||
96 | }) | ||
97 | |||
98 | it('Should remove an account blocklist', async function () { | ||
99 | blocklistServer.replace({ | ||
100 | data: [ | ||
101 | { | ||
102 | value: 'root@' + servers[1].host, | ||
103 | action: 'remove' | ||
104 | } | ||
105 | ] | ||
106 | }) | ||
107 | |||
108 | await wait(2000) | ||
109 | |||
110 | const { total } = await servers[0].videos.list() | ||
111 | expect(total).to.equal(2) | ||
112 | }) | ||
113 | |||
114 | it('Should auto mute an account, manually unmute it and do not remute it automatically', async function () { | ||
115 | this.timeout(20000) | ||
116 | |||
117 | const account = 'root@' + servers[1].host | ||
118 | |||
119 | blocklistServer.replace({ | ||
120 | data: [ | ||
121 | { | ||
122 | value: account, | ||
123 | updatedAt: new Date().toISOString() | ||
124 | } | ||
125 | ] | ||
126 | }) | ||
127 | |||
128 | await wait(2000) | ||
129 | |||
130 | { | ||
131 | const { total } = await servers[0].videos.list() | ||
132 | expect(total).to.equal(1) | ||
133 | } | ||
134 | |||
135 | await servers[0].blocklist.removeFromServerBlocklist({ account }) | ||
136 | |||
137 | { | ||
138 | const { total } = await servers[0].videos.list() | ||
139 | expect(total).to.equal(2) | ||
140 | } | ||
141 | |||
142 | await killallServers([ servers[0] ]) | ||
143 | await servers[0].run() | ||
144 | await wait(2000) | ||
145 | |||
146 | { | ||
147 | const { total } = await servers[0].videos.list() | ||
148 | expect(total).to.equal(2) | ||
149 | } | ||
150 | }) | ||
151 | |||
152 | it('Should not expose the auto mute list', async function () { | ||
153 | await makeGetRequest({ | ||
154 | url: servers[0].url, | ||
155 | path: '/plugins/auto-mute/router/api/v1/mute-list', | ||
156 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
157 | }) | ||
158 | }) | ||
159 | |||
160 | it('Should enable auto mute list', async function () { | ||
161 | await servers[0].plugins.updateSettings({ | ||
162 | npmName: 'peertube-plugin-auto-mute', | ||
163 | settings: { | ||
164 | 'blocklist-urls': '', | ||
165 | 'check-seconds-interval': 1, | ||
166 | 'expose-mute-list': true | ||
167 | } | ||
168 | }) | ||
169 | |||
170 | await makeGetRequest({ | ||
171 | url: servers[0].url, | ||
172 | path: '/plugins/auto-mute/router/api/v1/mute-list', | ||
173 | expectedStatus: HttpStatusCode.OK_200 | ||
174 | }) | ||
175 | }) | ||
176 | |||
177 | it('Should mute an account on server 1, and server 2 auto mutes it', async function () { | ||
178 | this.timeout(20000) | ||
179 | |||
180 | await servers[1].plugins.updateSettings({ | ||
181 | npmName: 'peertube-plugin-auto-mute', | ||
182 | settings: { | ||
183 | 'blocklist-urls': 'http://' + servers[0].host + autoMuteListPath, | ||
184 | 'check-seconds-interval': 1, | ||
185 | 'expose-mute-list': false | ||
186 | } | ||
187 | }) | ||
188 | |||
189 | await servers[0].blocklist.addToServerBlocklist({ account: 'root@' + servers[1].host }) | ||
190 | await servers[0].blocklist.addToMyBlocklist({ server: servers[1].host }) | ||
191 | |||
192 | const res = await makeGetRequest({ | ||
193 | url: servers[0].url, | ||
194 | path: '/plugins/auto-mute/router/api/v1/mute-list', | ||
195 | expectedStatus: HttpStatusCode.OK_200 | ||
196 | }) | ||
197 | |||
198 | const data = res.body.data | ||
199 | expect(data).to.have.lengthOf(1) | ||
200 | expect(data[0].updatedAt).to.exist | ||
201 | expect(data[0].value).to.equal('root@' + servers[1].host) | ||
202 | |||
203 | await wait(2000) | ||
204 | |||
205 | for (const server of servers) { | ||
206 | const { total } = await server.videos.list() | ||
207 | expect(total).to.equal(1) | ||
208 | } | ||
209 | }) | ||
210 | |||
211 | after(async function () { | ||
212 | await blocklistServer.terminate() | ||
213 | |||
214 | await cleanupTests(servers) | ||
215 | }) | ||
216 | }) | ||
diff --git a/packages/tests/src/external-plugins/index.ts b/packages/tests/src/external-plugins/index.ts new file mode 100644 index 000000000..815bbf1da --- /dev/null +++ b/packages/tests/src/external-plugins/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | import './akismet' | ||
2 | import './auth-ldap' | ||
3 | import './auto-block-videos' | ||
4 | import './auto-mute' | ||
diff --git a/packages/tests/src/feeds/feeds.ts b/packages/tests/src/feeds/feeds.ts new file mode 100644 index 000000000..7587bb34e --- /dev/null +++ b/packages/tests/src/feeds/feeds.ts | |||
@@ -0,0 +1,697 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import chaiJSONSChema from 'chai-json-schema' | ||
5 | import chaiXML from 'chai-xml' | ||
6 | import { XMLParser, XMLValidator } from 'fast-xml-parser' | ||
7 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createMultipleServers, | ||
11 | createSingleServer, | ||
12 | doubleFollow, | ||
13 | makeGetRequest, | ||
14 | makeRawRequest, | ||
15 | PeerTubeServer, | ||
16 | PluginsCommand, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultChannelAvatar, | ||
19 | setDefaultVideoChannel, | ||
20 | stopFfmpeg, | ||
21 | waitJobs | ||
22 | } from '@peertube/peertube-server-commands' | ||
23 | |||
24 | chai.use(chaiXML) | ||
25 | chai.use(chaiJSONSChema) | ||
26 | chai.config.includeStack = true | ||
27 | |||
28 | const expect = chai.expect | ||
29 | |||
30 | describe('Test syndication feeds', () => { | ||
31 | let servers: PeerTubeServer[] = [] | ||
32 | let serverHLSOnly: PeerTubeServer | ||
33 | |||
34 | let userAccessToken: string | ||
35 | let rootAccountId: number | ||
36 | let rootChannelId: number | ||
37 | |||
38 | let userAccountId: number | ||
39 | let userChannelId: number | ||
40 | let userFeedToken: string | ||
41 | |||
42 | let liveId: string | ||
43 | |||
44 | before(async function () { | ||
45 | this.timeout(120000) | ||
46 | |||
47 | // Run servers | ||
48 | servers = await createMultipleServers(2) | ||
49 | serverHLSOnly = await createSingleServer(3, { | ||
50 | transcoding: { | ||
51 | enabled: true, | ||
52 | web_videos: { enabled: false }, | ||
53 | hls: { enabled: true } | ||
54 | } | ||
55 | }) | ||
56 | |||
57 | await setAccessTokensToServers([ ...servers, serverHLSOnly ]) | ||
58 | await setDefaultChannelAvatar(servers[0]) | ||
59 | await setDefaultVideoChannel(servers) | ||
60 | await doubleFollow(servers[0], servers[1]) | ||
61 | |||
62 | await servers[0].config.enableLive({ allowReplay: false, transcoding: false }) | ||
63 | |||
64 | { | ||
65 | const user = await servers[0].users.getMyInfo() | ||
66 | rootAccountId = user.account.id | ||
67 | rootChannelId = user.videoChannels[0].id | ||
68 | } | ||
69 | |||
70 | { | ||
71 | userAccessToken = await servers[0].users.generateUserAndToken('john') | ||
72 | |||
73 | const user = await servers[0].users.getMyInfo({ token: userAccessToken }) | ||
74 | userAccountId = user.account.id | ||
75 | userChannelId = user.videoChannels[0].id | ||
76 | |||
77 | const token = await servers[0].users.getMyScopedTokens({ token: userAccessToken }) | ||
78 | userFeedToken = token.feedToken | ||
79 | } | ||
80 | |||
81 | { | ||
82 | await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'user video' } }) | ||
83 | } | ||
84 | |||
85 | { | ||
86 | const attributes = { | ||
87 | name: 'my super name for server 1', | ||
88 | description: 'my super description for server 1', | ||
89 | fixture: 'video_short.webm' | ||
90 | } | ||
91 | const { id } = await servers[0].videos.upload({ attributes }) | ||
92 | |||
93 | await servers[0].comments.createThread({ videoId: id, text: 'super comment 1' }) | ||
94 | await servers[0].comments.createThread({ videoId: id, text: 'super comment 2' }) | ||
95 | } | ||
96 | |||
97 | { | ||
98 | const attributes = { name: 'unlisted video', privacy: VideoPrivacy.UNLISTED } | ||
99 | const { id } = await servers[0].videos.upload({ attributes }) | ||
100 | |||
101 | await servers[0].comments.createThread({ videoId: id, text: 'comment on unlisted video' }) | ||
102 | } | ||
103 | |||
104 | { | ||
105 | const attributes = { name: 'password protected video', privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] } | ||
106 | const { id } = await servers[0].videos.upload({ attributes }) | ||
107 | |||
108 | await servers[0].comments.createThread({ videoId: id, text: 'comment on password protected video' }) | ||
109 | } | ||
110 | |||
111 | await serverHLSOnly.videos.upload({ attributes: { name: 'hls only video' } }) | ||
112 | |||
113 | await waitJobs([ ...servers, serverHLSOnly ]) | ||
114 | |||
115 | await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-podcast-custom-tags') }) | ||
116 | }) | ||
117 | |||
118 | describe('All feed', function () { | ||
119 | |||
120 | it('Should be well formed XML (covers RSS 2.0 and ATOM 1.0 endpoints)', async function () { | ||
121 | for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) { | ||
122 | const rss = await servers[0].feed.getXML({ feed, ignoreCache: true }) | ||
123 | expect(rss).xml.to.be.valid() | ||
124 | |||
125 | const atom = await servers[0].feed.getXML({ feed, format: 'atom', ignoreCache: true }) | ||
126 | expect(atom).xml.to.be.valid() | ||
127 | } | ||
128 | }) | ||
129 | |||
130 | it('Should be well formed XML (covers Podcast endpoint)', async function () { | ||
131 | const podcast = await servers[0].feed.getPodcastXML({ ignoreCache: true, channelId: rootChannelId }) | ||
132 | expect(podcast).xml.to.be.valid() | ||
133 | }) | ||
134 | |||
135 | it('Should be well formed JSON (covers JSON feed 1.0 endpoint)', async function () { | ||
136 | for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) { | ||
137 | const jsonText = await servers[0].feed.getJSON({ feed, ignoreCache: true }) | ||
138 | expect(JSON.parse(jsonText)).to.be.jsonSchema({ type: 'object' }) | ||
139 | } | ||
140 | }) | ||
141 | |||
142 | it('Should serve the endpoint with a classic request', async function () { | ||
143 | await makeGetRequest({ | ||
144 | url: servers[0].url, | ||
145 | path: '/feeds/videos.xml', | ||
146 | accept: 'application/xml', | ||
147 | expectedStatus: HttpStatusCode.OK_200 | ||
148 | }) | ||
149 | }) | ||
150 | |||
151 | it('Should refuse to serve the endpoint without accept header', async function () { | ||
152 | await makeGetRequest({ url: servers[0].url, path: '/feeds/videos.xml', expectedStatus: HttpStatusCode.NOT_ACCEPTABLE_406 }) | ||
153 | }) | ||
154 | }) | ||
155 | |||
156 | describe('Videos feed', function () { | ||
157 | |||
158 | describe('Podcast feed', function () { | ||
159 | |||
160 | it('Should contain a valid podcast:alternateEnclosure', async function () { | ||
161 | // Since podcast feeds should only work on the server they originate on, | ||
162 | // only test the first server where the videos reside | ||
163 | const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) | ||
164 | expect(XMLValidator.validate(rss)).to.be.true | ||
165 | |||
166 | const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) | ||
167 | const xmlDoc = parser.parse(rss) | ||
168 | |||
169 | const itemGuid = xmlDoc.rss.channel.item.guid | ||
170 | expect(itemGuid).to.exist | ||
171 | expect(itemGuid['@_isPermaLink']).to.equal(true) | ||
172 | |||
173 | const enclosure = xmlDoc.rss.channel.item.enclosure | ||
174 | expect(enclosure).to.exist | ||
175 | const alternateEnclosure = xmlDoc.rss.channel.item['podcast:alternateEnclosure'] | ||
176 | expect(alternateEnclosure).to.exist | ||
177 | |||
178 | expect(alternateEnclosure['@_type']).to.equal('video/webm') | ||
179 | expect(alternateEnclosure['@_length']).to.equal(218910) | ||
180 | expect(alternateEnclosure['@_lang']).to.equal('zh') | ||
181 | expect(alternateEnclosure['@_title']).to.equal('720p') | ||
182 | expect(alternateEnclosure['@_default']).to.equal(true) | ||
183 | |||
184 | expect(alternateEnclosure['podcast:source'][0]['@_uri']).to.contain('-720.webm') | ||
185 | expect(alternateEnclosure['podcast:source'][0]['@_uri']).to.equal(enclosure['@_url']) | ||
186 | expect(alternateEnclosure['podcast:source'][1]['@_uri']).to.contain('-720.torrent') | ||
187 | expect(alternateEnclosure['podcast:source'][1]['@_contentType']).to.equal('application/x-bittorrent') | ||
188 | expect(alternateEnclosure['podcast:source'][2]['@_uri']).to.contain('magnet:?') | ||
189 | }) | ||
190 | |||
191 | it('Should contain a valid podcast:alternateEnclosure with HLS only', async function () { | ||
192 | const rss = await serverHLSOnly.feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) | ||
193 | expect(XMLValidator.validate(rss)).to.be.true | ||
194 | |||
195 | const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) | ||
196 | const xmlDoc = parser.parse(rss) | ||
197 | |||
198 | const itemGuid = xmlDoc.rss.channel.item.guid | ||
199 | expect(itemGuid).to.exist | ||
200 | expect(itemGuid['@_isPermaLink']).to.equal(true) | ||
201 | |||
202 | const enclosure = xmlDoc.rss.channel.item.enclosure | ||
203 | const alternateEnclosure = xmlDoc.rss.channel.item['podcast:alternateEnclosure'] | ||
204 | expect(alternateEnclosure).to.exist | ||
205 | |||
206 | expect(alternateEnclosure['@_type']).to.equal('application/x-mpegURL') | ||
207 | expect(alternateEnclosure['@_lang']).to.equal('zh') | ||
208 | expect(alternateEnclosure['@_title']).to.equal('HLS') | ||
209 | expect(alternateEnclosure['@_default']).to.equal(true) | ||
210 | |||
211 | expect(alternateEnclosure['podcast:source']['@_uri']).to.contain('-master.m3u8') | ||
212 | expect(alternateEnclosure['podcast:source']['@_uri']).to.equal(enclosure['@_url']) | ||
213 | }) | ||
214 | |||
215 | it('Should contain a valid podcast:socialInteract', async function () { | ||
216 | const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) | ||
217 | expect(XMLValidator.validate(rss)).to.be.true | ||
218 | |||
219 | const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) | ||
220 | const xmlDoc = parser.parse(rss) | ||
221 | |||
222 | const item = xmlDoc.rss.channel.item | ||
223 | const socialInteract = item['podcast:socialInteract'] | ||
224 | expect(socialInteract).to.exist | ||
225 | expect(socialInteract['@_protocol']).to.equal('activitypub') | ||
226 | expect(socialInteract['@_uri']).to.exist | ||
227 | expect(socialInteract['@_accountUrl']).to.exist | ||
228 | }) | ||
229 | |||
230 | it('Should contain a valid support custom tags for plugins', async function () { | ||
231 | const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: userChannelId }) | ||
232 | expect(XMLValidator.validate(rss)).to.be.true | ||
233 | |||
234 | const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) | ||
235 | const xmlDoc = parser.parse(rss) | ||
236 | |||
237 | const fooTag = xmlDoc.rss.channel.fooTag | ||
238 | expect(fooTag).to.exist | ||
239 | expect(fooTag['@_bar']).to.equal('baz') | ||
240 | expect(fooTag['#text']).to.equal(42) | ||
241 | |||
242 | const bizzBuzzItem = xmlDoc.rss.channel['biz:buzzItem'] | ||
243 | expect(bizzBuzzItem).to.exist | ||
244 | |||
245 | let nestedTag = bizzBuzzItem.nestedTag | ||
246 | expect(nestedTag).to.exist | ||
247 | expect(nestedTag).to.equal('example nested tag') | ||
248 | |||
249 | const item = xmlDoc.rss.channel.item | ||
250 | const fizzTag = item.fizzTag | ||
251 | expect(fizzTag).to.exist | ||
252 | expect(fizzTag['@_bar']).to.equal('baz') | ||
253 | expect(fizzTag['#text']).to.equal(21) | ||
254 | |||
255 | const bizzBuzz = item['biz:buzz'] | ||
256 | expect(bizzBuzz).to.exist | ||
257 | |||
258 | nestedTag = bizzBuzz.nestedTag | ||
259 | expect(nestedTag).to.exist | ||
260 | expect(nestedTag).to.equal('example nested tag') | ||
261 | }) | ||
262 | |||
263 | it('Should contain a valid podcast:liveItem for live streams', async function () { | ||
264 | this.timeout(120000) | ||
265 | |||
266 | const { uuid } = await servers[0].live.create({ | ||
267 | fields: { | ||
268 | name: 'live-0', | ||
269 | privacy: VideoPrivacy.PUBLIC, | ||
270 | channelId: rootChannelId, | ||
271 | permanentLive: false | ||
272 | } | ||
273 | }) | ||
274 | liveId = uuid | ||
275 | |||
276 | const ffmpeg = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveId, copyCodecs: true, fixtureName: 'video_short.mp4' }) | ||
277 | await servers[0].live.waitUntilPublished({ videoId: liveId }) | ||
278 | |||
279 | const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) | ||
280 | expect(XMLValidator.validate(rss)).to.be.true | ||
281 | |||
282 | const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) | ||
283 | const xmlDoc = parser.parse(rss) | ||
284 | const liveItem = xmlDoc.rss.channel['podcast:liveItem'] | ||
285 | expect(liveItem.title).to.equal('live-0') | ||
286 | expect(liveItem.guid['@_isPermaLink']).to.equal(false) | ||
287 | expect(liveItem.guid['#text']).to.contain(`${uuid}_`) | ||
288 | expect(liveItem['@_status']).to.equal('live') | ||
289 | |||
290 | const enclosure = liveItem.enclosure | ||
291 | const alternateEnclosure = liveItem['podcast:alternateEnclosure'] | ||
292 | expect(alternateEnclosure).to.exist | ||
293 | expect(alternateEnclosure['@_type']).to.equal('application/x-mpegURL') | ||
294 | expect(alternateEnclosure['@_title']).to.equal('HLS live stream') | ||
295 | expect(alternateEnclosure['@_default']).to.equal(true) | ||
296 | |||
297 | expect(alternateEnclosure['podcast:source']['@_uri']).to.contain('/master.m3u8') | ||
298 | expect(alternateEnclosure['podcast:source']['@_uri']).to.equal(enclosure['@_url']) | ||
299 | |||
300 | await stopFfmpeg(ffmpeg) | ||
301 | |||
302 | await servers[0].live.waitUntilEnded({ videoId: liveId }) | ||
303 | |||
304 | await waitJobs(servers) | ||
305 | }) | ||
306 | }) | ||
307 | |||
308 | describe('JSON feed', function () { | ||
309 | |||
310 | it('Should contain a valid \'attachments\' object', async function () { | ||
311 | for (const server of servers) { | ||
312 | const json = await server.feed.getJSON({ feed: 'videos', ignoreCache: true }) | ||
313 | const jsonObj = JSON.parse(json) | ||
314 | expect(jsonObj.items.length).to.be.equal(2) | ||
315 | expect(jsonObj.items[0].attachments).to.exist | ||
316 | expect(jsonObj.items[0].attachments.length).to.be.eq(1) | ||
317 | expect(jsonObj.items[0].attachments[0].mime_type).to.be.eq('application/x-bittorrent') | ||
318 | expect(jsonObj.items[0].attachments[0].size_in_bytes).to.be.eq(218910) | ||
319 | expect(jsonObj.items[0].attachments[0].url).to.contain('720.torrent') | ||
320 | } | ||
321 | }) | ||
322 | |||
323 | it('Should filter by account', async function () { | ||
324 | { | ||
325 | const json = await servers[0].feed.getJSON({ feed: 'videos', query: { accountId: rootAccountId }, ignoreCache: true }) | ||
326 | const jsonObj = JSON.parse(json) | ||
327 | expect(jsonObj.items.length).to.be.equal(1) | ||
328 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | ||
329 | expect(jsonObj.items[0].author.name).to.equal('Main root channel') | ||
330 | } | ||
331 | |||
332 | { | ||
333 | const json = await servers[0].feed.getJSON({ feed: 'videos', query: { accountId: userAccountId }, ignoreCache: true }) | ||
334 | const jsonObj = JSON.parse(json) | ||
335 | expect(jsonObj.items.length).to.be.equal(1) | ||
336 | expect(jsonObj.items[0].title).to.equal('user video') | ||
337 | expect(jsonObj.items[0].author.name).to.equal('Main john channel') | ||
338 | } | ||
339 | |||
340 | for (const server of servers) { | ||
341 | { | ||
342 | const json = await server.feed.getJSON({ feed: 'videos', query: { accountName: 'root@' + servers[0].host }, ignoreCache: true }) | ||
343 | const jsonObj = JSON.parse(json) | ||
344 | expect(jsonObj.items.length).to.be.equal(1) | ||
345 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | ||
346 | } | ||
347 | |||
348 | { | ||
349 | const json = await server.feed.getJSON({ feed: 'videos', query: { accountName: 'john@' + servers[0].host }, ignoreCache: true }) | ||
350 | const jsonObj = JSON.parse(json) | ||
351 | expect(jsonObj.items.length).to.be.equal(1) | ||
352 | expect(jsonObj.items[0].title).to.equal('user video') | ||
353 | } | ||
354 | } | ||
355 | }) | ||
356 | |||
357 | it('Should filter by video channel', async function () { | ||
358 | { | ||
359 | const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: rootChannelId }, ignoreCache: true }) | ||
360 | const jsonObj = JSON.parse(json) | ||
361 | expect(jsonObj.items.length).to.be.equal(1) | ||
362 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | ||
363 | expect(jsonObj.items[0].author.name).to.equal('Main root channel') | ||
364 | } | ||
365 | |||
366 | { | ||
367 | const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: userChannelId }, ignoreCache: true }) | ||
368 | const jsonObj = JSON.parse(json) | ||
369 | expect(jsonObj.items.length).to.be.equal(1) | ||
370 | expect(jsonObj.items[0].title).to.equal('user video') | ||
371 | expect(jsonObj.items[0].author.name).to.equal('Main john channel') | ||
372 | } | ||
373 | |||
374 | for (const server of servers) { | ||
375 | { | ||
376 | const query = { videoChannelName: 'root_channel@' + servers[0].host } | ||
377 | const json = await server.feed.getJSON({ feed: 'videos', query, ignoreCache: true }) | ||
378 | const jsonObj = JSON.parse(json) | ||
379 | expect(jsonObj.items.length).to.be.equal(1) | ||
380 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | ||
381 | } | ||
382 | |||
383 | { | ||
384 | const query = { videoChannelName: 'john_channel@' + servers[0].host } | ||
385 | const json = await server.feed.getJSON({ feed: 'videos', query, ignoreCache: true }) | ||
386 | const jsonObj = JSON.parse(json) | ||
387 | expect(jsonObj.items.length).to.be.equal(1) | ||
388 | expect(jsonObj.items[0].title).to.equal('user video') | ||
389 | } | ||
390 | } | ||
391 | }) | ||
392 | |||
393 | it('Should correctly have videos feed with HLS only', async function () { | ||
394 | this.timeout(120000) | ||
395 | |||
396 | const json = await serverHLSOnly.feed.getJSON({ feed: 'videos', ignoreCache: true }) | ||
397 | const jsonObj = JSON.parse(json) | ||
398 | expect(jsonObj.items.length).to.be.equal(1) | ||
399 | expect(jsonObj.items[0].attachments).to.exist | ||
400 | expect(jsonObj.items[0].attachments.length).to.be.eq(4) | ||
401 | |||
402 | for (let i = 0; i < 4; i++) { | ||
403 | expect(jsonObj.items[0].attachments[i].mime_type).to.be.eq('application/x-bittorrent') | ||
404 | expect(jsonObj.items[0].attachments[i].size_in_bytes).to.be.greaterThan(0) | ||
405 | expect(jsonObj.items[0].attachments[i].url).to.exist | ||
406 | } | ||
407 | }) | ||
408 | |||
409 | it('Should not display waiting live videos', async function () { | ||
410 | const { uuid } = await servers[0].live.create({ | ||
411 | fields: { | ||
412 | name: 'live', | ||
413 | privacy: VideoPrivacy.PUBLIC, | ||
414 | channelId: rootChannelId | ||
415 | } | ||
416 | }) | ||
417 | liveId = uuid | ||
418 | |||
419 | const json = await servers[0].feed.getJSON({ feed: 'videos', ignoreCache: true }) | ||
420 | |||
421 | const jsonObj = JSON.parse(json) | ||
422 | expect(jsonObj.items.length).to.be.equal(2) | ||
423 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | ||
424 | expect(jsonObj.items[1].title).to.equal('user video') | ||
425 | }) | ||
426 | |||
427 | it('Should display published live videos', async function () { | ||
428 | this.timeout(120000) | ||
429 | |||
430 | const ffmpeg = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveId, copyCodecs: true, fixtureName: 'video_short.mp4' }) | ||
431 | await servers[0].live.waitUntilPublished({ videoId: liveId }) | ||
432 | |||
433 | const json = await servers[0].feed.getJSON({ feed: 'videos', ignoreCache: true }) | ||
434 | |||
435 | const jsonObj = JSON.parse(json) | ||
436 | expect(jsonObj.items.length).to.be.equal(3) | ||
437 | expect(jsonObj.items[0].title).to.equal('live') | ||
438 | expect(jsonObj.items[1].title).to.equal('my super name for server 1') | ||
439 | expect(jsonObj.items[2].title).to.equal('user video') | ||
440 | |||
441 | await stopFfmpeg(ffmpeg) | ||
442 | }) | ||
443 | |||
444 | it('Should have the channel avatar as feed icon', async function () { | ||
445 | const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: rootChannelId }, ignoreCache: true }) | ||
446 | |||
447 | const jsonObj = JSON.parse(json) | ||
448 | const imageUrl = jsonObj.icon | ||
449 | expect(imageUrl).to.include('/lazy-static/avatars/') | ||
450 | await makeRawRequest({ url: imageUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
451 | }) | ||
452 | }) | ||
453 | }) | ||
454 | |||
455 | describe('Video comments feed', function () { | ||
456 | |||
457 | it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted/password protected videos', async function () { | ||
458 | for (const server of servers) { | ||
459 | const json = await server.feed.getJSON({ feed: 'video-comments', ignoreCache: true }) | ||
460 | |||
461 | const jsonObj = JSON.parse(json) | ||
462 | expect(jsonObj.items.length).to.be.equal(2) | ||
463 | expect(jsonObj.items[0].content_html).to.contain('<p>super comment 2</p>') | ||
464 | expect(jsonObj.items[1].content_html).to.contain('<p>super comment 1</p>') | ||
465 | } | ||
466 | }) | ||
467 | |||
468 | it('Should not list comments from muted accounts or instances', async function () { | ||
469 | this.timeout(30000) | ||
470 | |||
471 | const remoteHandle = 'root@' + servers[0].host | ||
472 | |||
473 | await servers[1].blocklist.addToServerBlocklist({ account: remoteHandle }) | ||
474 | |||
475 | { | ||
476 | const json = await servers[1].feed.getJSON({ feed: 'video-comments', ignoreCache: true }) | ||
477 | const jsonObj = JSON.parse(json) | ||
478 | expect(jsonObj.items.length).to.be.equal(0) | ||
479 | } | ||
480 | |||
481 | await servers[1].blocklist.removeFromServerBlocklist({ account: remoteHandle }) | ||
482 | |||
483 | { | ||
484 | const videoUUID = (await servers[1].videos.quickUpload({ name: 'server 2' })).uuid | ||
485 | await waitJobs(servers) | ||
486 | await servers[0].comments.createThread({ videoId: videoUUID, text: 'super comment' }) | ||
487 | await waitJobs(servers) | ||
488 | |||
489 | const json = await servers[1].feed.getJSON({ feed: 'video-comments', ignoreCache: true }) | ||
490 | const jsonObj = JSON.parse(json) | ||
491 | expect(jsonObj.items.length).to.be.equal(3) | ||
492 | } | ||
493 | |||
494 | await servers[1].blocklist.addToMyBlocklist({ account: remoteHandle }) | ||
495 | |||
496 | { | ||
497 | const json = await servers[1].feed.getJSON({ feed: 'video-comments', ignoreCache: true }) | ||
498 | const jsonObj = JSON.parse(json) | ||
499 | expect(jsonObj.items.length).to.be.equal(2) | ||
500 | } | ||
501 | }) | ||
502 | }) | ||
503 | |||
504 | describe('Video feed from my subscriptions', function () { | ||
505 | let feeduserAccountId: number | ||
506 | let feeduserFeedToken: string | ||
507 | |||
508 | it('Should list no videos for a user with no videos and no subscriptions', async function () { | ||
509 | const attr = { username: 'feeduser', password: 'password' } | ||
510 | await servers[0].users.create({ username: attr.username, password: attr.password }) | ||
511 | const feeduserAccessToken = await servers[0].login.getAccessToken(attr) | ||
512 | |||
513 | { | ||
514 | const user = await servers[0].users.getMyInfo({ token: feeduserAccessToken }) | ||
515 | feeduserAccountId = user.account.id | ||
516 | } | ||
517 | |||
518 | { | ||
519 | const token = await servers[0].users.getMyScopedTokens({ token: feeduserAccessToken }) | ||
520 | feeduserFeedToken = token.feedToken | ||
521 | } | ||
522 | |||
523 | { | ||
524 | const body = await servers[0].videos.listMySubscriptionVideos({ token: feeduserAccessToken }) | ||
525 | expect(body.total).to.equal(0) | ||
526 | |||
527 | const query = { accountId: feeduserAccountId, token: feeduserFeedToken } | ||
528 | const json = await servers[0].feed.getJSON({ feed: 'subscriptions', query, ignoreCache: true }) | ||
529 | const jsonObj = JSON.parse(json) | ||
530 | expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos | ||
531 | } | ||
532 | }) | ||
533 | |||
534 | it('Should fail with an invalid token', async function () { | ||
535 | const query = { accountId: feeduserAccountId, token: 'toto' } | ||
536 | await servers[0].feed.getJSON({ feed: 'subscriptions', query, expectedStatus: HttpStatusCode.FORBIDDEN_403, ignoreCache: true }) | ||
537 | }) | ||
538 | |||
539 | it('Should fail with a token of another user', async function () { | ||
540 | const query = { accountId: feeduserAccountId, token: userFeedToken } | ||
541 | await servers[0].feed.getJSON({ feed: 'subscriptions', query, expectedStatus: HttpStatusCode.FORBIDDEN_403, ignoreCache: true }) | ||
542 | }) | ||
543 | |||
544 | it('Should list no videos for a user with videos but no subscriptions', async function () { | ||
545 | const body = await servers[0].videos.listMySubscriptionVideos({ token: userAccessToken }) | ||
546 | expect(body.total).to.equal(0) | ||
547 | |||
548 | const query = { accountId: userAccountId, token: userFeedToken } | ||
549 | const json = await servers[0].feed.getJSON({ feed: 'subscriptions', query, ignoreCache: true }) | ||
550 | const jsonObj = JSON.parse(json) | ||
551 | expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos | ||
552 | }) | ||
553 | |||
554 | it('Should list self videos for a user with a subscription to themselves', async function () { | ||
555 | this.timeout(30000) | ||
556 | |||
557 | await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'john_channel@' + servers[0].host }) | ||
558 | await waitJobs(servers) | ||
559 | |||
560 | { | ||
561 | const body = await servers[0].videos.listMySubscriptionVideos({ token: userAccessToken }) | ||
562 | expect(body.total).to.equal(1) | ||
563 | expect(body.data[0].name).to.equal('user video') | ||
564 | |||
565 | const query = { accountId: userAccountId, token: userFeedToken } | ||
566 | const json = await servers[0].feed.getJSON({ feed: 'subscriptions', query, ignoreCache: true }) | ||
567 | const jsonObj = JSON.parse(json) | ||
568 | expect(jsonObj.items.length).to.be.equal(1) // subscribed to self, it should not list the instance's videos but list john's | ||
569 | } | ||
570 | }) | ||
571 | |||
572 | it('Should list videos of a user\'s subscription', async function () { | ||
573 | this.timeout(30000) | ||
574 | |||
575 | await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@' + servers[0].host }) | ||
576 | await waitJobs(servers) | ||
577 | |||
578 | { | ||
579 | const body = await servers[0].videos.listMySubscriptionVideos({ token: userAccessToken }) | ||
580 | expect(body.total).to.equal(2, 'there should be 2 videos part of the subscription') | ||
581 | |||
582 | const query = { accountId: userAccountId, token: userFeedToken } | ||
583 | const json = await servers[0].feed.getJSON({ feed: 'subscriptions', query, ignoreCache: true }) | ||
584 | const jsonObj = JSON.parse(json) | ||
585 | expect(jsonObj.items.length).to.be.equal(2) // subscribed to root, it should not list the instance's videos but list root/john's | ||
586 | } | ||
587 | }) | ||
588 | |||
589 | it('Should renew the token, and so have an invalid old token', async function () { | ||
590 | await servers[0].users.renewMyScopedTokens({ token: userAccessToken }) | ||
591 | |||
592 | const query = { accountId: userAccountId, token: userFeedToken } | ||
593 | await servers[0].feed.getJSON({ feed: 'subscriptions', query, expectedStatus: HttpStatusCode.FORBIDDEN_403, ignoreCache: true }) | ||
594 | }) | ||
595 | |||
596 | it('Should succeed with the new token', async function () { | ||
597 | const token = await servers[0].users.getMyScopedTokens({ token: userAccessToken }) | ||
598 | userFeedToken = token.feedToken | ||
599 | |||
600 | const query = { accountId: userAccountId, token: userFeedToken } | ||
601 | await servers[0].feed.getJSON({ feed: 'subscriptions', query, ignoreCache: true }) | ||
602 | }) | ||
603 | |||
604 | }) | ||
605 | |||
606 | describe('Cache', function () { | ||
607 | const uuids: string[] = [] | ||
608 | |||
609 | function doPodcastRequest () { | ||
610 | return makeGetRequest({ | ||
611 | url: servers[0].url, | ||
612 | path: '/feeds/podcast/videos.xml', | ||
613 | query: { videoChannelId: servers[0].store.channel.id }, | ||
614 | accept: 'application/xml', | ||
615 | expectedStatus: HttpStatusCode.OK_200 | ||
616 | }) | ||
617 | } | ||
618 | |||
619 | function doVideosRequest (query: { [id: string]: string } = {}) { | ||
620 | return makeGetRequest({ | ||
621 | url: servers[0].url, | ||
622 | path: '/feeds/videos.xml', | ||
623 | query, | ||
624 | accept: 'application/xml', | ||
625 | expectedStatus: HttpStatusCode.OK_200 | ||
626 | }) | ||
627 | } | ||
628 | |||
629 | before(async function () { | ||
630 | { | ||
631 | const { uuid } = await servers[0].videos.quickUpload({ name: 'cache 1' }) | ||
632 | uuids.push(uuid) | ||
633 | } | ||
634 | |||
635 | { | ||
636 | const { uuid } = await servers[0].videos.quickUpload({ name: 'cache 2' }) | ||
637 | uuids.push(uuid) | ||
638 | } | ||
639 | }) | ||
640 | |||
641 | it('Should serve the videos endpoint as a cached request', async function () { | ||
642 | await doVideosRequest() | ||
643 | |||
644 | const res = await doVideosRequest() | ||
645 | |||
646 | expect(res.headers['x-api-cache-cached']).to.equal('true') | ||
647 | }) | ||
648 | |||
649 | it('Should not serve the videos endpoint as a cached request', async function () { | ||
650 | const res = await doVideosRequest({ v: '186' }) | ||
651 | |||
652 | expect(res.headers['x-api-cache-cached']).to.not.exist | ||
653 | }) | ||
654 | |||
655 | it('Should invalidate the podcast feed cache after video deletion', async function () { | ||
656 | await doPodcastRequest() | ||
657 | |||
658 | { | ||
659 | const res = await doPodcastRequest() | ||
660 | expect(res.headers['x-api-cache-cached']).to.exist | ||
661 | } | ||
662 | |||
663 | await servers[0].videos.remove({ id: uuids[0] }) | ||
664 | |||
665 | { | ||
666 | const res = await doPodcastRequest() | ||
667 | expect(res.headers['x-api-cache-cached']).to.not.exist | ||
668 | } | ||
669 | }) | ||
670 | |||
671 | it('Should invalidate the podcast feed cache after video deletion, even after server restart', async function () { | ||
672 | this.timeout(120000) | ||
673 | |||
674 | await doPodcastRequest() | ||
675 | |||
676 | { | ||
677 | const res = await doPodcastRequest() | ||
678 | expect(res.headers['x-api-cache-cached']).to.exist | ||
679 | } | ||
680 | |||
681 | await servers[0].kill() | ||
682 | await servers[0].run() | ||
683 | |||
684 | await servers[0].videos.remove({ id: uuids[1] }) | ||
685 | |||
686 | const res = await doPodcastRequest() | ||
687 | expect(res.headers['x-api-cache-cached']).to.not.exist | ||
688 | }) | ||
689 | |||
690 | }) | ||
691 | |||
692 | after(async function () { | ||
693 | await servers[0].plugins.uninstall({ npmName: 'peertube-plugin-test-podcast-custom-tags' }) | ||
694 | |||
695 | await cleanupTests([ ...servers, serverHLSOnly ]) | ||
696 | }) | ||
697 | }) | ||
diff --git a/packages/tests/src/feeds/index.ts b/packages/tests/src/feeds/index.ts new file mode 100644 index 000000000..aa6236a91 --- /dev/null +++ b/packages/tests/src/feeds/index.ts | |||
@@ -0,0 +1 @@ | |||
import './feeds' | |||
diff --git a/packages/tests/src/misc-endpoints.ts b/packages/tests/src/misc-endpoints.ts new file mode 100644 index 000000000..0067578ed --- /dev/null +++ b/packages/tests/src/misc-endpoints.ts | |||
@@ -0,0 +1,243 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { writeJson } from 'fs-extra/esm' | ||
5 | import { join } from 'path' | ||
6 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | makeGetRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | import { expectLogDoesNotContain } from './shared/checks.js' | ||
15 | |||
16 | describe('Test misc endpoints', function () { | ||
17 | let server: PeerTubeServer | ||
18 | let wellKnownPath: string | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(120000) | ||
22 | |||
23 | server = await createSingleServer(1) | ||
24 | |||
25 | await setAccessTokensToServers([ server ]) | ||
26 | |||
27 | wellKnownPath = server.getDirectoryPath('well-known') | ||
28 | }) | ||
29 | |||
30 | describe('Test a well known endpoints', function () { | ||
31 | |||
32 | it('Should get security.txt', async function () { | ||
33 | const res = await makeGetRequest({ | ||
34 | url: server.url, | ||
35 | path: '/.well-known/security.txt', | ||
36 | expectedStatus: HttpStatusCode.OK_200 | ||
37 | }) | ||
38 | |||
39 | expect(res.text).to.contain('security issue') | ||
40 | }) | ||
41 | |||
42 | it('Should get nodeinfo', async function () { | ||
43 | const res = await makeGetRequest({ | ||
44 | url: server.url, | ||
45 | path: '/.well-known/nodeinfo', | ||
46 | expectedStatus: HttpStatusCode.OK_200 | ||
47 | }) | ||
48 | |||
49 | expect(res.body.links).to.be.an('array') | ||
50 | expect(res.body.links).to.have.lengthOf(1) | ||
51 | expect(res.body.links[0].rel).to.equal('http://nodeinfo.diaspora.software/ns/schema/2.0') | ||
52 | }) | ||
53 | |||
54 | it('Should get dnt policy text', async function () { | ||
55 | const res = await makeGetRequest({ | ||
56 | url: server.url, | ||
57 | path: '/.well-known/dnt-policy.txt', | ||
58 | expectedStatus: HttpStatusCode.OK_200 | ||
59 | }) | ||
60 | |||
61 | expect(res.text).to.contain('http://www.w3.org/TR/tracking-dnt') | ||
62 | }) | ||
63 | |||
64 | it('Should get dnt policy', async function () { | ||
65 | const res = await makeGetRequest({ | ||
66 | url: server.url, | ||
67 | path: '/.well-known/dnt', | ||
68 | expectedStatus: HttpStatusCode.OK_200 | ||
69 | }) | ||
70 | |||
71 | expect(res.body.tracking).to.equal('N') | ||
72 | }) | ||
73 | |||
74 | it('Should get change-password location', async function () { | ||
75 | const res = await makeGetRequest({ | ||
76 | url: server.url, | ||
77 | path: '/.well-known/change-password', | ||
78 | expectedStatus: HttpStatusCode.FOUND_302 | ||
79 | }) | ||
80 | |||
81 | expect(res.header.location).to.equal('/my-account/settings') | ||
82 | }) | ||
83 | |||
84 | it('Should test webfinger', async function () { | ||
85 | const resource = 'acct:peertube@' + server.host | ||
86 | const accountUrl = server.url + '/accounts/peertube' | ||
87 | |||
88 | const res = await makeGetRequest({ | ||
89 | url: server.url, | ||
90 | path: '/.well-known/webfinger?resource=' + resource, | ||
91 | expectedStatus: HttpStatusCode.OK_200 | ||
92 | }) | ||
93 | |||
94 | const data = res.body | ||
95 | |||
96 | expect(data.subject).to.equal(resource) | ||
97 | expect(data.aliases).to.contain(accountUrl) | ||
98 | |||
99 | const self = data.links.find(l => l.rel === 'self') | ||
100 | expect(self).to.exist | ||
101 | expect(self.type).to.equal('application/activity+json') | ||
102 | expect(self.href).to.equal(accountUrl) | ||
103 | |||
104 | const remoteInteract = data.links.find(l => l.rel === 'http://ostatus.org/schema/1.0/subscribe') | ||
105 | expect(remoteInteract).to.exist | ||
106 | expect(remoteInteract.template).to.equal(server.url + '/remote-interaction?uri={uri}') | ||
107 | }) | ||
108 | |||
109 | it('Should return 404 for non-existing files in /.well-known', async function () { | ||
110 | await makeGetRequest({ | ||
111 | url: server.url, | ||
112 | path: '/.well-known/non-existing-file', | ||
113 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
114 | }) | ||
115 | }) | ||
116 | |||
117 | it('Should return custom file from /.well-known', async function () { | ||
118 | const filename = 'existing-file.json' | ||
119 | |||
120 | await writeJson(join(wellKnownPath, filename), { iThink: 'therefore I am' }) | ||
121 | |||
122 | const { body } = await makeGetRequest({ | ||
123 | url: server.url, | ||
124 | path: '/.well-known/' + filename, | ||
125 | expectedStatus: HttpStatusCode.OK_200 | ||
126 | }) | ||
127 | |||
128 | expect(body.iThink).to.equal('therefore I am') | ||
129 | }) | ||
130 | }) | ||
131 | |||
132 | describe('Test classic static endpoints', function () { | ||
133 | |||
134 | it('Should get robots.txt', async function () { | ||
135 | const res = await makeGetRequest({ | ||
136 | url: server.url, | ||
137 | path: '/robots.txt', | ||
138 | expectedStatus: HttpStatusCode.OK_200 | ||
139 | }) | ||
140 | |||
141 | expect(res.text).to.contain('User-agent') | ||
142 | }) | ||
143 | |||
144 | it('Should get security.txt', async function () { | ||
145 | await makeGetRequest({ | ||
146 | url: server.url, | ||
147 | path: '/security.txt', | ||
148 | expectedStatus: HttpStatusCode.MOVED_PERMANENTLY_301 | ||
149 | }) | ||
150 | }) | ||
151 | |||
152 | it('Should get nodeinfo', async function () { | ||
153 | const res = await makeGetRequest({ | ||
154 | url: server.url, | ||
155 | path: '/nodeinfo/2.0.json', | ||
156 | expectedStatus: HttpStatusCode.OK_200 | ||
157 | }) | ||
158 | |||
159 | expect(res.body.software.name).to.equal('peertube') | ||
160 | expect(res.body.usage.users.activeMonth).to.equal(1) | ||
161 | expect(res.body.usage.users.activeHalfyear).to.equal(1) | ||
162 | }) | ||
163 | }) | ||
164 | |||
165 | describe('Test bots endpoints', function () { | ||
166 | |||
167 | it('Should get the empty sitemap', async function () { | ||
168 | const res = await makeGetRequest({ | ||
169 | url: server.url, | ||
170 | path: '/sitemap.xml', | ||
171 | expectedStatus: HttpStatusCode.OK_200 | ||
172 | }) | ||
173 | |||
174 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
175 | expect(res.text).to.contain('<url><loc>' + server.url + '/about/instance</loc></url>') | ||
176 | }) | ||
177 | |||
178 | it('Should get the empty cached sitemap', async function () { | ||
179 | const res = await makeGetRequest({ | ||
180 | url: server.url, | ||
181 | path: '/sitemap.xml', | ||
182 | expectedStatus: HttpStatusCode.OK_200 | ||
183 | }) | ||
184 | |||
185 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
186 | expect(res.text).to.contain('<url><loc>' + server.url + '/about/instance</loc></url>') | ||
187 | }) | ||
188 | |||
189 | it('Should add videos, channel and accounts and get sitemap', async function () { | ||
190 | this.timeout(35000) | ||
191 | |||
192 | await server.videos.upload({ attributes: { name: 'video 1', nsfw: false } }) | ||
193 | await server.videos.upload({ attributes: { name: 'video 2', nsfw: false } }) | ||
194 | await server.videos.upload({ attributes: { name: 'video 3', privacy: VideoPrivacy.PRIVATE } }) | ||
195 | |||
196 | await server.channels.create({ attributes: { name: 'channel1', displayName: 'channel 1' } }) | ||
197 | await server.channels.create({ attributes: { name: 'channel2', displayName: 'channel 2' } }) | ||
198 | |||
199 | await server.users.create({ username: 'user1', password: 'password' }) | ||
200 | await server.users.create({ username: 'user2', password: 'password' }) | ||
201 | |||
202 | const res = await makeGetRequest({ | ||
203 | url: server.url, | ||
204 | path: '/sitemap.xml?t=1', // avoid using cache | ||
205 | expectedStatus: HttpStatusCode.OK_200 | ||
206 | }) | ||
207 | |||
208 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
209 | expect(res.text).to.contain('<url><loc>' + server.url + '/about/instance</loc></url>') | ||
210 | |||
211 | expect(res.text).to.contain('<video:title>video 1</video:title>') | ||
212 | expect(res.text).to.contain('<video:title>video 2</video:title>') | ||
213 | expect(res.text).to.not.contain('<video:title>video 3</video:title>') | ||
214 | |||
215 | expect(res.text).to.contain('<url><loc>' + server.url + '/video-channels/channel1</loc></url>') | ||
216 | expect(res.text).to.contain('<url><loc>' + server.url + '/video-channels/channel2</loc></url>') | ||
217 | |||
218 | expect(res.text).to.contain('<url><loc>' + server.url + '/accounts/user1</loc></url>') | ||
219 | expect(res.text).to.contain('<url><loc>' + server.url + '/accounts/user2</loc></url>') | ||
220 | }) | ||
221 | |||
222 | it('Should not fail with big title/description videos', async function () { | ||
223 | const name = 'v'.repeat(115) | ||
224 | |||
225 | await server.videos.upload({ attributes: { name, description: 'd'.repeat(2500), nsfw: false } }) | ||
226 | |||
227 | const res = await makeGetRequest({ | ||
228 | url: server.url, | ||
229 | path: '/sitemap.xml?t=2', // avoid using cache | ||
230 | expectedStatus: HttpStatusCode.OK_200 | ||
231 | }) | ||
232 | |||
233 | await expectLogDoesNotContain(server, 'Warning in sitemap generation') | ||
234 | await expectLogDoesNotContain(server, 'Error in sitemap generation') | ||
235 | |||
236 | expect(res.text).to.contain(`<video:title>${'v'.repeat(97)}...</video:title>`) | ||
237 | }) | ||
238 | }) | ||
239 | |||
240 | after(async function () { | ||
241 | await cleanupTests([ server ]) | ||
242 | }) | ||
243 | }) | ||
diff --git a/packages/tests/src/peertube-runner/client-cli.ts b/packages/tests/src/peertube-runner/client-cli.ts new file mode 100644 index 000000000..814b7f13a --- /dev/null +++ b/packages/tests/src/peertube-runner/client-cli.ts | |||
@@ -0,0 +1,76 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { PeerTubeRunnerProcess } from '@tests/shared/peertube-runner-process.js' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test peertube-runner program client CLI', function () { | ||
14 | let server: PeerTubeServer | ||
15 | let peertubeRunner: PeerTubeRunnerProcess | ||
16 | |||
17 | before(async function () { | ||
18 | this.timeout(120_000) | ||
19 | |||
20 | server = await createSingleServer(1) | ||
21 | |||
22 | await setAccessTokensToServers([ server ]) | ||
23 | await setDefaultVideoChannel([ server ]) | ||
24 | |||
25 | await server.config.enableRemoteTranscoding() | ||
26 | |||
27 | peertubeRunner = new PeerTubeRunnerProcess(server) | ||
28 | await peertubeRunner.runServer() | ||
29 | }) | ||
30 | |||
31 | it('Should not have PeerTube instance listed', async function () { | ||
32 | const data = await peertubeRunner.listRegisteredPeerTubeInstances() | ||
33 | |||
34 | expect(data).to.not.contain(server.url) | ||
35 | }) | ||
36 | |||
37 | it('Should register a new PeerTube instance', async function () { | ||
38 | const registrationToken = await server.runnerRegistrationTokens.getFirstRegistrationToken() | ||
39 | |||
40 | await peertubeRunner.registerPeerTubeInstance({ | ||
41 | registrationToken, | ||
42 | runnerName: 'my super runner', | ||
43 | runnerDescription: 'super description' | ||
44 | }) | ||
45 | }) | ||
46 | |||
47 | it('Should list this new PeerTube instance', async function () { | ||
48 | const data = await peertubeRunner.listRegisteredPeerTubeInstances() | ||
49 | |||
50 | expect(data).to.contain(server.url) | ||
51 | expect(data).to.contain('my super runner') | ||
52 | expect(data).to.contain('super description') | ||
53 | }) | ||
54 | |||
55 | it('Should still have the configuration after a restart', async function () { | ||
56 | peertubeRunner.kill() | ||
57 | |||
58 | await peertubeRunner.runServer() | ||
59 | }) | ||
60 | |||
61 | it('Should unregister the PeerTube instance', async function () { | ||
62 | await peertubeRunner.unregisterPeerTubeInstance({ runnerName: 'my super runner' }) | ||
63 | }) | ||
64 | |||
65 | it('Should not have PeerTube instance listed', async function () { | ||
66 | const data = await peertubeRunner.listRegisteredPeerTubeInstances() | ||
67 | |||
68 | expect(data).to.not.contain(server.url) | ||
69 | }) | ||
70 | |||
71 | after(async function () { | ||
72 | peertubeRunner.kill() | ||
73 | |||
74 | await cleanupTests([ server ]) | ||
75 | }) | ||
76 | }) | ||
diff --git a/packages/tests/src/peertube-runner/index.ts b/packages/tests/src/peertube-runner/index.ts new file mode 100644 index 000000000..29f21694f --- /dev/null +++ b/packages/tests/src/peertube-runner/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './client-cli.js' | ||
2 | export * from './live-transcoding.js' | ||
3 | export * from './studio-transcoding.js' | ||
4 | export * from './vod-transcoding.js' | ||
diff --git a/packages/tests/src/peertube-runner/live-transcoding.ts b/packages/tests/src/peertube-runner/live-transcoding.ts new file mode 100644 index 000000000..9351bc5e2 --- /dev/null +++ b/packages/tests/src/peertube-runner/live-transcoding.ts | |||
@@ -0,0 +1,200 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { expect } from 'chai' | ||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | findExternalSavedVideo, | ||
11 | makeRawRequest, | ||
12 | ObjectStorageCommand, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | stopFfmpeg, | ||
17 | waitJobs, | ||
18 | waitUntilLivePublishedOnAllServers, | ||
19 | waitUntilLiveWaitingOnAllServers | ||
20 | } from '@peertube/peertube-server-commands' | ||
21 | import { expectStartWith } from '@tests/shared/checks.js' | ||
22 | import { checkPeerTubeRunnerCacheIsEmpty } from '@tests/shared/directories.js' | ||
23 | import { testLiveVideoResolutions } from '@tests/shared/live.js' | ||
24 | import { PeerTubeRunnerProcess } from '@tests/shared/peertube-runner-process.js' | ||
25 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
26 | |||
27 | describe('Test Live transcoding in peertube-runner program', function () { | ||
28 | let servers: PeerTubeServer[] = [] | ||
29 | let peertubeRunner: PeerTubeRunnerProcess | ||
30 | let sqlCommandServer1: SQLCommand | ||
31 | |||
32 | function runSuite (options: { | ||
33 | objectStorage?: ObjectStorageCommand | ||
34 | } = {}) { | ||
35 | const { objectStorage } = options | ||
36 | |||
37 | it('Should enable transcoding without additional resolutions', async function () { | ||
38 | this.timeout(120000) | ||
39 | |||
40 | const { video } = await servers[0].live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) | ||
41 | |||
42 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: video.uuid }) | ||
43 | await waitUntilLivePublishedOnAllServers(servers, video.uuid) | ||
44 | await waitJobs(servers) | ||
45 | |||
46 | await testLiveVideoResolutions({ | ||
47 | originServer: servers[0], | ||
48 | sqlCommand: sqlCommandServer1, | ||
49 | servers, | ||
50 | liveVideoId: video.uuid, | ||
51 | resolutions: [ 720, 480, 360, 240, 144 ], | ||
52 | objectStorage, | ||
53 | transcoded: true | ||
54 | }) | ||
55 | |||
56 | await stopFfmpeg(ffmpegCommand) | ||
57 | |||
58 | await waitUntilLiveWaitingOnAllServers(servers, video.uuid) | ||
59 | await servers[0].videos.remove({ id: video.id }) | ||
60 | }) | ||
61 | |||
62 | it('Should transcode audio only RTMP stream', async function () { | ||
63 | this.timeout(120000) | ||
64 | |||
65 | const { video } = await servers[0].live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.UNLISTED }) | ||
66 | |||
67 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: video.uuid, fixtureName: 'video_short_no_audio.mp4' }) | ||
68 | await waitUntilLivePublishedOnAllServers(servers, video.uuid) | ||
69 | await waitJobs(servers) | ||
70 | |||
71 | await stopFfmpeg(ffmpegCommand) | ||
72 | |||
73 | await waitUntilLiveWaitingOnAllServers(servers, video.uuid) | ||
74 | await servers[0].videos.remove({ id: video.id }) | ||
75 | }) | ||
76 | |||
77 | it('Should save a replay', async function () { | ||
78 | this.timeout(240000) | ||
79 | |||
80 | const { video } = await servers[0].live.quickCreate({ permanentLive: true, saveReplay: true }) | ||
81 | |||
82 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: video.uuid }) | ||
83 | await waitUntilLivePublishedOnAllServers(servers, video.uuid) | ||
84 | |||
85 | await testLiveVideoResolutions({ | ||
86 | originServer: servers[0], | ||
87 | sqlCommand: sqlCommandServer1, | ||
88 | servers, | ||
89 | liveVideoId: video.uuid, | ||
90 | resolutions: [ 720, 480, 360, 240, 144 ], | ||
91 | objectStorage, | ||
92 | transcoded: true | ||
93 | }) | ||
94 | |||
95 | await stopFfmpeg(ffmpegCommand) | ||
96 | |||
97 | await waitUntilLiveWaitingOnAllServers(servers, video.uuid) | ||
98 | await waitJobs(servers) | ||
99 | |||
100 | const session = await servers[0].live.findLatestSession({ videoId: video.uuid }) | ||
101 | expect(session.endingProcessed).to.be.true | ||
102 | expect(session.endDate).to.exist | ||
103 | expect(session.saveReplay).to.be.true | ||
104 | |||
105 | const videoLiveDetails = await servers[0].videos.get({ id: video.uuid }) | ||
106 | const replay = await findExternalSavedVideo(servers[0], videoLiveDetails) | ||
107 | |||
108 | for (const server of servers) { | ||
109 | const video = await server.videos.get({ id: replay.uuid }) | ||
110 | |||
111 | expect(video.files).to.have.lengthOf(0) | ||
112 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
113 | |||
114 | const files = video.streamingPlaylists[0].files | ||
115 | expect(files).to.have.lengthOf(5) | ||
116 | |||
117 | for (const file of files) { | ||
118 | if (objectStorage) { | ||
119 | expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
120 | } | ||
121 | |||
122 | await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
123 | } | ||
124 | } | ||
125 | }) | ||
126 | } | ||
127 | |||
128 | before(async function () { | ||
129 | this.timeout(120_000) | ||
130 | |||
131 | servers = await createMultipleServers(2) | ||
132 | |||
133 | await setAccessTokensToServers(servers) | ||
134 | await setDefaultVideoChannel(servers) | ||
135 | |||
136 | await doubleFollow(servers[0], servers[1]) | ||
137 | |||
138 | sqlCommandServer1 = new SQLCommand(servers[0]) | ||
139 | |||
140 | await servers[0].config.enableRemoteTranscoding() | ||
141 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true }) | ||
142 | await servers[0].config.enableLive({ allowReplay: true, resolutions: 'max', transcoding: true }) | ||
143 | |||
144 | const registrationToken = await servers[0].runnerRegistrationTokens.getFirstRegistrationToken() | ||
145 | |||
146 | peertubeRunner = new PeerTubeRunnerProcess(servers[0]) | ||
147 | await peertubeRunner.runServer() | ||
148 | await peertubeRunner.registerPeerTubeInstance({ registrationToken, runnerName: 'runner' }) | ||
149 | }) | ||
150 | |||
151 | describe('With lives on local filesystem storage', function () { | ||
152 | |||
153 | before(async function () { | ||
154 | await servers[0].config.enableTranscoding({ webVideo: true, hls: false, with0p: true }) | ||
155 | }) | ||
156 | |||
157 | runSuite() | ||
158 | }) | ||
159 | |||
160 | describe('With lives on object storage', function () { | ||
161 | if (areMockObjectStorageTestsDisabled()) return | ||
162 | |||
163 | const objectStorage = new ObjectStorageCommand() | ||
164 | |||
165 | before(async function () { | ||
166 | await objectStorage.prepareDefaultMockBuckets() | ||
167 | |||
168 | await servers[0].kill() | ||
169 | |||
170 | await servers[0].run(objectStorage.getDefaultMockConfig()) | ||
171 | |||
172 | // Wait for peertube runner socket reconnection | ||
173 | await wait(1500) | ||
174 | }) | ||
175 | |||
176 | runSuite({ objectStorage }) | ||
177 | |||
178 | after(async function () { | ||
179 | await objectStorage.cleanupMock() | ||
180 | }) | ||
181 | }) | ||
182 | |||
183 | describe('Check cleanup', function () { | ||
184 | |||
185 | it('Should have an empty cache directory', async function () { | ||
186 | await checkPeerTubeRunnerCacheIsEmpty(peertubeRunner) | ||
187 | }) | ||
188 | }) | ||
189 | |||
190 | after(async function () { | ||
191 | if (peertubeRunner) { | ||
192 | await peertubeRunner.unregisterPeerTubeInstance({ runnerName: 'runner' }) | ||
193 | peertubeRunner.kill() | ||
194 | } | ||
195 | |||
196 | if (sqlCommandServer1) await sqlCommandServer1.cleanup() | ||
197 | |||
198 | await cleanupTests(servers) | ||
199 | }) | ||
200 | }) | ||
diff --git a/packages/tests/src/peertube-runner/studio-transcoding.ts b/packages/tests/src/peertube-runner/studio-transcoding.ts new file mode 100644 index 000000000..50e61091a --- /dev/null +++ b/packages/tests/src/peertube-runner/studio-transcoding.ts | |||
@@ -0,0 +1,127 @@ | |||
1 | |||
2 | import { expect } from 'chai' | ||
3 | import { getAllFiles, wait } from '@peertube/peertube-core-utils' | ||
4 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | ObjectStorageCommand, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | VideoStudioCommand, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | import { expectStartWith, checkVideoDuration } from '@tests/shared/checks.js' | ||
17 | import { checkPeerTubeRunnerCacheIsEmpty } from '@tests/shared/directories.js' | ||
18 | import { PeerTubeRunnerProcess } from '@tests/shared/peertube-runner-process.js' | ||
19 | |||
20 | describe('Test studio transcoding in peertube-runner program', function () { | ||
21 | let servers: PeerTubeServer[] = [] | ||
22 | let peertubeRunner: PeerTubeRunnerProcess | ||
23 | |||
24 | function runSuite (options: { | ||
25 | objectStorage?: ObjectStorageCommand | ||
26 | } = {}) { | ||
27 | const { objectStorage } = options | ||
28 | |||
29 | it('Should run a complex studio transcoding', async function () { | ||
30 | this.timeout(120000) | ||
31 | |||
32 | const { uuid } = await servers[0].videos.quickUpload({ name: 'mp4', fixture: 'video_short.mp4' }) | ||
33 | await waitJobs(servers) | ||
34 | |||
35 | const video = await servers[0].videos.get({ id: uuid }) | ||
36 | const oldFileUrls = getAllFiles(video).map(f => f.fileUrl) | ||
37 | |||
38 | await servers[0].videoStudio.createEditionTasks({ videoId: uuid, tasks: VideoStudioCommand.getComplexTask() }) | ||
39 | await waitJobs(servers, { runnerJobs: true }) | ||
40 | |||
41 | for (const server of servers) { | ||
42 | const video = await server.videos.get({ id: uuid }) | ||
43 | const files = getAllFiles(video) | ||
44 | |||
45 | for (const f of files) { | ||
46 | expect(oldFileUrls).to.not.include(f.fileUrl) | ||
47 | } | ||
48 | |||
49 | if (objectStorage) { | ||
50 | for (const webVideoFile of video.files) { | ||
51 | expectStartWith(webVideoFile.fileUrl, objectStorage.getMockWebVideosBaseUrl()) | ||
52 | } | ||
53 | |||
54 | for (const hlsFile of video.streamingPlaylists[0].files) { | ||
55 | expectStartWith(hlsFile.fileUrl, objectStorage.getMockPlaylistBaseUrl()) | ||
56 | } | ||
57 | } | ||
58 | |||
59 | await checkVideoDuration(server, uuid, 9) | ||
60 | } | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | before(async function () { | ||
65 | this.timeout(120_000) | ||
66 | |||
67 | servers = await createMultipleServers(2) | ||
68 | |||
69 | await setAccessTokensToServers(servers) | ||
70 | await setDefaultVideoChannel(servers) | ||
71 | |||
72 | await doubleFollow(servers[0], servers[1]) | ||
73 | |||
74 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) | ||
75 | await servers[0].config.enableStudio() | ||
76 | await servers[0].config.enableRemoteStudio() | ||
77 | |||
78 | const registrationToken = await servers[0].runnerRegistrationTokens.getFirstRegistrationToken() | ||
79 | |||
80 | peertubeRunner = new PeerTubeRunnerProcess(servers[0]) | ||
81 | await peertubeRunner.runServer() | ||
82 | await peertubeRunner.registerPeerTubeInstance({ registrationToken, runnerName: 'runner' }) | ||
83 | }) | ||
84 | |||
85 | describe('With videos on local filesystem storage', function () { | ||
86 | runSuite() | ||
87 | }) | ||
88 | |||
89 | describe('With videos on object storage', function () { | ||
90 | if (areMockObjectStorageTestsDisabled()) return | ||
91 | |||
92 | const objectStorage = new ObjectStorageCommand() | ||
93 | |||
94 | before(async function () { | ||
95 | await objectStorage.prepareDefaultMockBuckets() | ||
96 | |||
97 | await servers[0].kill() | ||
98 | |||
99 | await servers[0].run(objectStorage.getDefaultMockConfig()) | ||
100 | |||
101 | // Wait for peertube runner socket reconnection | ||
102 | await wait(1500) | ||
103 | }) | ||
104 | |||
105 | runSuite({ objectStorage }) | ||
106 | |||
107 | after(async function () { | ||
108 | await objectStorage.cleanupMock() | ||
109 | }) | ||
110 | }) | ||
111 | |||
112 | describe('Check cleanup', function () { | ||
113 | |||
114 | it('Should have an empty cache directory', async function () { | ||
115 | await checkPeerTubeRunnerCacheIsEmpty(peertubeRunner) | ||
116 | }) | ||
117 | }) | ||
118 | |||
119 | after(async function () { | ||
120 | if (peertubeRunner) { | ||
121 | await peertubeRunner.unregisterPeerTubeInstance({ runnerName: 'runner' }) | ||
122 | peertubeRunner.kill() | ||
123 | } | ||
124 | |||
125 | await cleanupTests(servers) | ||
126 | }) | ||
127 | }) | ||
diff --git a/packages/tests/src/peertube-runner/vod-transcoding.ts b/packages/tests/src/peertube-runner/vod-transcoding.ts new file mode 100644 index 000000000..ff5cefe36 --- /dev/null +++ b/packages/tests/src/peertube-runner/vod-transcoding.ts | |||
@@ -0,0 +1,349 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | import { expect } from 'chai' | ||
3 | import { getAllFiles, wait } from '@peertube/peertube-core-utils' | ||
4 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | import { checkPeerTubeRunnerCacheIsEmpty } from '@tests/shared/directories.js' | ||
17 | import { PeerTubeRunnerProcess } from '@tests/shared/peertube-runner-process.js' | ||
18 | import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' | ||
19 | import { completeWebVideoFilesCheck } from '@tests/shared/videos.js' | ||
20 | |||
21 | describe('Test VOD transcoding in peertube-runner program', function () { | ||
22 | let servers: PeerTubeServer[] = [] | ||
23 | let peertubeRunner: PeerTubeRunnerProcess | ||
24 | |||
25 | function runSuite (options: { | ||
26 | webVideoEnabled: boolean | ||
27 | hlsEnabled: boolean | ||
28 | objectStorage?: ObjectStorageCommand | ||
29 | }) { | ||
30 | const { webVideoEnabled, hlsEnabled, objectStorage } = options | ||
31 | |||
32 | const objectStorageBaseUrlWebVideo = objectStorage | ||
33 | ? objectStorage.getMockWebVideosBaseUrl() | ||
34 | : undefined | ||
35 | |||
36 | const objectStorageBaseUrlHLS = objectStorage | ||
37 | ? objectStorage.getMockPlaylistBaseUrl() | ||
38 | : undefined | ||
39 | |||
40 | it('Should upload a classic video mp4 and transcode it', async function () { | ||
41 | this.timeout(120000) | ||
42 | |||
43 | const { uuid } = await servers[0].videos.quickUpload({ name: 'mp4', fixture: 'video_short.mp4' }) | ||
44 | |||
45 | await waitJobs(servers, { runnerJobs: true }) | ||
46 | |||
47 | for (const server of servers) { | ||
48 | if (webVideoEnabled) { | ||
49 | await completeWebVideoFilesCheck({ | ||
50 | server, | ||
51 | originServer: servers[0], | ||
52 | fixture: 'video_short.mp4', | ||
53 | videoUUID: uuid, | ||
54 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, | ||
55 | files: [ | ||
56 | { resolution: 0 }, | ||
57 | { resolution: 144 }, | ||
58 | { resolution: 240 }, | ||
59 | { resolution: 360 }, | ||
60 | { resolution: 480 }, | ||
61 | { resolution: 720 } | ||
62 | ] | ||
63 | }) | ||
64 | } | ||
65 | |||
66 | if (hlsEnabled) { | ||
67 | await completeCheckHlsPlaylist({ | ||
68 | hlsOnly: !webVideoEnabled, | ||
69 | servers, | ||
70 | videoUUID: uuid, | ||
71 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | ||
72 | resolutions: [ 720, 480, 360, 240, 144, 0 ] | ||
73 | }) | ||
74 | } | ||
75 | } | ||
76 | }) | ||
77 | |||
78 | it('Should upload a webm video and transcode it', async function () { | ||
79 | this.timeout(120000) | ||
80 | |||
81 | const { uuid } = await servers[0].videos.quickUpload({ name: 'mp4', fixture: 'video_short.webm' }) | ||
82 | |||
83 | await waitJobs(servers, { runnerJobs: true }) | ||
84 | |||
85 | for (const server of servers) { | ||
86 | if (webVideoEnabled) { | ||
87 | await completeWebVideoFilesCheck({ | ||
88 | server, | ||
89 | originServer: servers[0], | ||
90 | fixture: 'video_short.webm', | ||
91 | videoUUID: uuid, | ||
92 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, | ||
93 | files: [ | ||
94 | { resolution: 0 }, | ||
95 | { resolution: 144 }, | ||
96 | { resolution: 240 }, | ||
97 | { resolution: 360 }, | ||
98 | { resolution: 480 }, | ||
99 | { resolution: 720 } | ||
100 | ] | ||
101 | }) | ||
102 | } | ||
103 | |||
104 | if (hlsEnabled) { | ||
105 | await completeCheckHlsPlaylist({ | ||
106 | hlsOnly: !webVideoEnabled, | ||
107 | servers, | ||
108 | videoUUID: uuid, | ||
109 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | ||
110 | resolutions: [ 720, 480, 360, 240, 144, 0 ] | ||
111 | }) | ||
112 | } | ||
113 | } | ||
114 | }) | ||
115 | |||
116 | it('Should upload an audio only video and transcode it', async function () { | ||
117 | this.timeout(120000) | ||
118 | |||
119 | const attributes = { name: 'audio_without_preview', fixture: 'sample.ogg' } | ||
120 | const { uuid } = await servers[0].videos.upload({ attributes, mode: 'resumable' }) | ||
121 | |||
122 | await waitJobs(servers, { runnerJobs: true }) | ||
123 | |||
124 | for (const server of servers) { | ||
125 | if (webVideoEnabled) { | ||
126 | await completeWebVideoFilesCheck({ | ||
127 | server, | ||
128 | originServer: servers[0], | ||
129 | fixture: 'sample.ogg', | ||
130 | videoUUID: uuid, | ||
131 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, | ||
132 | files: [ | ||
133 | { resolution: 0 }, | ||
134 | { resolution: 144 }, | ||
135 | { resolution: 240 }, | ||
136 | { resolution: 360 }, | ||
137 | { resolution: 480 } | ||
138 | ] | ||
139 | }) | ||
140 | } | ||
141 | |||
142 | if (hlsEnabled) { | ||
143 | await completeCheckHlsPlaylist({ | ||
144 | hlsOnly: !webVideoEnabled, | ||
145 | servers, | ||
146 | videoUUID: uuid, | ||
147 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | ||
148 | resolutions: [ 480, 360, 240, 144, 0 ] | ||
149 | }) | ||
150 | } | ||
151 | } | ||
152 | }) | ||
153 | |||
154 | it('Should upload a private video and transcode it', async function () { | ||
155 | this.timeout(120000) | ||
156 | |||
157 | const { uuid } = await servers[0].videos.quickUpload({ name: 'mp4', fixture: 'video_short.mp4', privacy: VideoPrivacy.PRIVATE }) | ||
158 | |||
159 | await waitJobs(servers, { runnerJobs: true }) | ||
160 | |||
161 | if (webVideoEnabled) { | ||
162 | await completeWebVideoFilesCheck({ | ||
163 | server: servers[0], | ||
164 | originServer: servers[0], | ||
165 | fixture: 'video_short.mp4', | ||
166 | videoUUID: uuid, | ||
167 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, | ||
168 | files: [ | ||
169 | { resolution: 0 }, | ||
170 | { resolution: 144 }, | ||
171 | { resolution: 240 }, | ||
172 | { resolution: 360 }, | ||
173 | { resolution: 480 }, | ||
174 | { resolution: 720 } | ||
175 | ] | ||
176 | }) | ||
177 | } | ||
178 | |||
179 | if (hlsEnabled) { | ||
180 | await completeCheckHlsPlaylist({ | ||
181 | hlsOnly: !webVideoEnabled, | ||
182 | servers: [ servers[0] ], | ||
183 | videoUUID: uuid, | ||
184 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | ||
185 | resolutions: [ 720, 480, 360, 240, 144, 0 ] | ||
186 | }) | ||
187 | } | ||
188 | }) | ||
189 | |||
190 | it('Should transcode videos on manual run', async function () { | ||
191 | this.timeout(120000) | ||
192 | |||
193 | await servers[0].config.disableTranscoding() | ||
194 | |||
195 | const { uuid } = await servers[0].videos.quickUpload({ name: 'manual transcoding', fixture: 'video_short.mp4' }) | ||
196 | await waitJobs(servers, { runnerJobs: true }) | ||
197 | |||
198 | { | ||
199 | const video = await servers[0].videos.get({ id: uuid }) | ||
200 | expect(getAllFiles(video)).to.have.lengthOf(1) | ||
201 | } | ||
202 | |||
203 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true }) | ||
204 | |||
205 | await servers[0].videos.runTranscoding({ transcodingType: 'web-video', videoId: uuid }) | ||
206 | await waitJobs(servers, { runnerJobs: true }) | ||
207 | |||
208 | await completeWebVideoFilesCheck({ | ||
209 | server: servers[0], | ||
210 | originServer: servers[0], | ||
211 | fixture: 'video_short.mp4', | ||
212 | videoUUID: uuid, | ||
213 | objectStorageBaseUrl: objectStorageBaseUrlWebVideo, | ||
214 | files: [ | ||
215 | { resolution: 0 }, | ||
216 | { resolution: 144 }, | ||
217 | { resolution: 240 }, | ||
218 | { resolution: 360 }, | ||
219 | { resolution: 480 }, | ||
220 | { resolution: 720 } | ||
221 | ] | ||
222 | }) | ||
223 | |||
224 | await servers[0].videos.runTranscoding({ transcodingType: 'hls', videoId: uuid }) | ||
225 | await waitJobs(servers, { runnerJobs: true }) | ||
226 | |||
227 | await completeCheckHlsPlaylist({ | ||
228 | hlsOnly: false, | ||
229 | servers: [ servers[0] ], | ||
230 | videoUUID: uuid, | ||
231 | objectStorageBaseUrl: objectStorageBaseUrlHLS, | ||
232 | resolutions: [ 720, 480, 360, 240, 144, 0 ] | ||
233 | }) | ||
234 | }) | ||
235 | } | ||
236 | |||
237 | before(async function () { | ||
238 | this.timeout(120_000) | ||
239 | |||
240 | servers = await createMultipleServers(2) | ||
241 | |||
242 | await setAccessTokensToServers(servers) | ||
243 | await setDefaultVideoChannel(servers) | ||
244 | |||
245 | await doubleFollow(servers[0], servers[1]) | ||
246 | |||
247 | await servers[0].config.enableRemoteTranscoding() | ||
248 | |||
249 | const registrationToken = await servers[0].runnerRegistrationTokens.getFirstRegistrationToken() | ||
250 | |||
251 | peertubeRunner = new PeerTubeRunnerProcess(servers[0]) | ||
252 | await peertubeRunner.runServer() | ||
253 | await peertubeRunner.registerPeerTubeInstance({ registrationToken, runnerName: 'runner' }) | ||
254 | }) | ||
255 | |||
256 | describe('With videos on local filesystem storage', function () { | ||
257 | |||
258 | describe('Web video only enabled', function () { | ||
259 | |||
260 | before(async function () { | ||
261 | await servers[0].config.enableTranscoding({ webVideo: true, hls: false, with0p: true }) | ||
262 | }) | ||
263 | |||
264 | runSuite({ webVideoEnabled: true, hlsEnabled: false }) | ||
265 | }) | ||
266 | |||
267 | describe('HLS videos only enabled', function () { | ||
268 | |||
269 | before(async function () { | ||
270 | await servers[0].config.enableTranscoding({ webVideo: false, hls: true, with0p: true }) | ||
271 | }) | ||
272 | |||
273 | runSuite({ webVideoEnabled: false, hlsEnabled: true }) | ||
274 | }) | ||
275 | |||
276 | describe('Web video & HLS enabled', function () { | ||
277 | |||
278 | before(async function () { | ||
279 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true }) | ||
280 | }) | ||
281 | |||
282 | runSuite({ webVideoEnabled: true, hlsEnabled: true }) | ||
283 | }) | ||
284 | }) | ||
285 | |||
286 | describe('With videos on object storage', function () { | ||
287 | if (areMockObjectStorageTestsDisabled()) return | ||
288 | |||
289 | const objectStorage = new ObjectStorageCommand() | ||
290 | |||
291 | before(async function () { | ||
292 | await objectStorage.prepareDefaultMockBuckets() | ||
293 | |||
294 | await servers[0].kill() | ||
295 | |||
296 | await servers[0].run(objectStorage.getDefaultMockConfig()) | ||
297 | |||
298 | // Wait for peertube runner socket reconnection | ||
299 | await wait(1500) | ||
300 | }) | ||
301 | |||
302 | describe('Web video only enabled', function () { | ||
303 | |||
304 | before(async function () { | ||
305 | await servers[0].config.enableTranscoding({ webVideo: true, hls: false, with0p: true }) | ||
306 | }) | ||
307 | |||
308 | runSuite({ webVideoEnabled: true, hlsEnabled: false, objectStorage }) | ||
309 | }) | ||
310 | |||
311 | describe('HLS videos only enabled', function () { | ||
312 | |||
313 | before(async function () { | ||
314 | await servers[0].config.enableTranscoding({ webVideo: false, hls: true, with0p: true }) | ||
315 | }) | ||
316 | |||
317 | runSuite({ webVideoEnabled: false, hlsEnabled: true, objectStorage }) | ||
318 | }) | ||
319 | |||
320 | describe('Web video & HLS enabled', function () { | ||
321 | |||
322 | before(async function () { | ||
323 | await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true }) | ||
324 | }) | ||
325 | |||
326 | runSuite({ webVideoEnabled: true, hlsEnabled: true, objectStorage }) | ||
327 | }) | ||
328 | |||
329 | after(async function () { | ||
330 | await objectStorage.cleanupMock() | ||
331 | }) | ||
332 | }) | ||
333 | |||
334 | describe('Check cleanup', function () { | ||
335 | |||
336 | it('Should have an empty cache directory', async function () { | ||
337 | await checkPeerTubeRunnerCacheIsEmpty(peertubeRunner) | ||
338 | }) | ||
339 | }) | ||
340 | |||
341 | after(async function () { | ||
342 | if (peertubeRunner) { | ||
343 | await peertubeRunner.unregisterPeerTubeInstance({ runnerName: 'runner' }) | ||
344 | peertubeRunner.kill() | ||
345 | } | ||
346 | |||
347 | await cleanupTests(servers) | ||
348 | }) | ||
349 | }) | ||
diff --git a/packages/tests/src/plugins/action-hooks.ts b/packages/tests/src/plugins/action-hooks.ts new file mode 100644 index 000000000..136c7671b --- /dev/null +++ b/packages/tests/src/plugins/action-hooks.ts | |||
@@ -0,0 +1,298 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | killallServers, | ||
9 | PeerTubeServer, | ||
10 | PluginsCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | stopFfmpeg, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test plugin action hooks', function () { | ||
18 | let servers: PeerTubeServer[] | ||
19 | let videoUUID: string | ||
20 | let threadId: number | ||
21 | |||
22 | function checkHook (hook: ServerHookName, strictCount = true, count = 1) { | ||
23 | return servers[0].servers.waitUntilLog('Run hook ' + hook, count, strictCount) | ||
24 | } | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(120000) | ||
28 | |||
29 | servers = await createMultipleServers(2) | ||
30 | await setAccessTokensToServers(servers) | ||
31 | await setDefaultVideoChannel(servers) | ||
32 | |||
33 | await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() }) | ||
34 | |||
35 | await killallServers([ servers[0] ]) | ||
36 | |||
37 | await servers[0].run({ | ||
38 | live: { | ||
39 | enabled: true | ||
40 | } | ||
41 | }) | ||
42 | |||
43 | await servers[0].config.enableFileUpdate() | ||
44 | |||
45 | await doubleFollow(servers[0], servers[1]) | ||
46 | }) | ||
47 | |||
48 | describe('Application hooks', function () { | ||
49 | it('Should run action:application.listening', async function () { | ||
50 | await checkHook('action:application.listening') | ||
51 | }) | ||
52 | }) | ||
53 | |||
54 | describe('Videos hooks', function () { | ||
55 | |||
56 | it('Should run action:api.video.uploaded', async function () { | ||
57 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' } }) | ||
58 | videoUUID = uuid | ||
59 | |||
60 | await checkHook('action:api.video.uploaded') | ||
61 | }) | ||
62 | |||
63 | it('Should run action:api.video.updated', async function () { | ||
64 | await servers[0].videos.update({ id: videoUUID, attributes: { name: 'video updated' } }) | ||
65 | |||
66 | await checkHook('action:api.video.updated') | ||
67 | }) | ||
68 | |||
69 | it('Should run action:api.video.viewed', async function () { | ||
70 | await servers[0].views.simulateView({ id: videoUUID }) | ||
71 | |||
72 | await checkHook('action:api.video.viewed') | ||
73 | }) | ||
74 | |||
75 | it('Should run action:api.video.file-updated', async function () { | ||
76 | await servers[0].videos.replaceSourceFile({ videoId: videoUUID, fixture: 'video_short.mp4' }) | ||
77 | |||
78 | await checkHook('action:api.video.file-updated') | ||
79 | }) | ||
80 | |||
81 | it('Should run action:api.video.deleted', async function () { | ||
82 | await servers[0].videos.remove({ id: videoUUID }) | ||
83 | |||
84 | await checkHook('action:api.video.deleted') | ||
85 | }) | ||
86 | |||
87 | after(async function () { | ||
88 | const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) | ||
89 | videoUUID = uuid | ||
90 | }) | ||
91 | }) | ||
92 | |||
93 | describe('Video channel hooks', function () { | ||
94 | const channelName = 'my_super_channel' | ||
95 | |||
96 | it('Should run action:api.video-channel.created', async function () { | ||
97 | await servers[0].channels.create({ attributes: { name: channelName } }) | ||
98 | |||
99 | await checkHook('action:api.video-channel.created') | ||
100 | }) | ||
101 | |||
102 | it('Should run action:api.video-channel.updated', async function () { | ||
103 | await servers[0].channels.update({ channelName, attributes: { displayName: 'my display name' } }) | ||
104 | |||
105 | await checkHook('action:api.video-channel.updated') | ||
106 | }) | ||
107 | |||
108 | it('Should run action:api.video-channel.deleted', async function () { | ||
109 | await servers[0].channels.delete({ channelName }) | ||
110 | |||
111 | await checkHook('action:api.video-channel.deleted') | ||
112 | }) | ||
113 | }) | ||
114 | |||
115 | describe('Live hooks', function () { | ||
116 | |||
117 | it('Should run action:api.live-video.created', async function () { | ||
118 | const attributes = { | ||
119 | name: 'live', | ||
120 | privacy: VideoPrivacy.PUBLIC, | ||
121 | channelId: servers[0].store.channel.id | ||
122 | } | ||
123 | |||
124 | await servers[0].live.create({ fields: attributes }) | ||
125 | |||
126 | await checkHook('action:api.live-video.created') | ||
127 | }) | ||
128 | |||
129 | it('Should run action:live.video.state.updated', async function () { | ||
130 | this.timeout(60000) | ||
131 | |||
132 | const attributes = { | ||
133 | name: 'live', | ||
134 | privacy: VideoPrivacy.PUBLIC, | ||
135 | channelId: servers[0].store.channel.id | ||
136 | } | ||
137 | |||
138 | const { uuid: liveVideoId } = await servers[0].live.create({ fields: attributes }) | ||
139 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId }) | ||
140 | await servers[0].live.waitUntilPublished({ videoId: liveVideoId }) | ||
141 | await waitJobs(servers) | ||
142 | |||
143 | await checkHook('action:live.video.state.updated', true, 1) | ||
144 | |||
145 | await stopFfmpeg(ffmpegCommand) | ||
146 | await servers[0].live.waitUntilEnded({ videoId: liveVideoId }) | ||
147 | await waitJobs(servers) | ||
148 | |||
149 | await checkHook('action:live.video.state.updated', true, 2) | ||
150 | }) | ||
151 | }) | ||
152 | |||
153 | describe('Comments hooks', function () { | ||
154 | it('Should run action:api.video-thread.created', async function () { | ||
155 | const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' }) | ||
156 | threadId = created.id | ||
157 | |||
158 | await checkHook('action:api.video-thread.created') | ||
159 | }) | ||
160 | |||
161 | it('Should run action:api.video-comment-reply.created', async function () { | ||
162 | await servers[0].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text: 'reply' }) | ||
163 | |||
164 | await checkHook('action:api.video-comment-reply.created') | ||
165 | }) | ||
166 | |||
167 | it('Should run action:api.video-comment.deleted', async function () { | ||
168 | await servers[0].comments.delete({ videoId: videoUUID, commentId: threadId }) | ||
169 | |||
170 | await checkHook('action:api.video-comment.deleted') | ||
171 | }) | ||
172 | }) | ||
173 | |||
174 | describe('Captions hooks', function () { | ||
175 | it('Should run action:api.video-caption.created', async function () { | ||
176 | await servers[0].captions.add({ videoId: videoUUID, language: 'en', fixture: 'subtitle-good.srt' }) | ||
177 | |||
178 | await checkHook('action:api.video-caption.created') | ||
179 | }) | ||
180 | |||
181 | it('Should run action:api.video-caption.deleted', async function () { | ||
182 | await servers[0].captions.delete({ videoId: videoUUID, language: 'en' }) | ||
183 | |||
184 | await checkHook('action:api.video-caption.deleted') | ||
185 | }) | ||
186 | }) | ||
187 | |||
188 | describe('Users hooks', function () { | ||
189 | let userId: number | ||
190 | |||
191 | it('Should run action:api.user.registered', async function () { | ||
192 | await servers[0].registrations.register({ username: 'registered_user' }) | ||
193 | |||
194 | await checkHook('action:api.user.registered') | ||
195 | }) | ||
196 | |||
197 | it('Should run action:api.user.created', async function () { | ||
198 | const user = await servers[0].users.create({ username: 'created_user' }) | ||
199 | userId = user.id | ||
200 | |||
201 | await checkHook('action:api.user.created') | ||
202 | }) | ||
203 | |||
204 | it('Should run action:api.user.oauth2-got-token', async function () { | ||
205 | await servers[0].login.login({ user: { username: 'created_user' } }) | ||
206 | |||
207 | await checkHook('action:api.user.oauth2-got-token') | ||
208 | }) | ||
209 | |||
210 | it('Should run action:api.user.blocked', async function () { | ||
211 | await servers[0].users.banUser({ userId }) | ||
212 | |||
213 | await checkHook('action:api.user.blocked') | ||
214 | }) | ||
215 | |||
216 | it('Should run action:api.user.unblocked', async function () { | ||
217 | await servers[0].users.unbanUser({ userId }) | ||
218 | |||
219 | await checkHook('action:api.user.unblocked') | ||
220 | }) | ||
221 | |||
222 | it('Should run action:api.user.updated', async function () { | ||
223 | await servers[0].users.update({ userId, videoQuota: 50 }) | ||
224 | |||
225 | await checkHook('action:api.user.updated') | ||
226 | }) | ||
227 | |||
228 | it('Should run action:api.user.deleted', async function () { | ||
229 | await servers[0].users.remove({ userId }) | ||
230 | |||
231 | await checkHook('action:api.user.deleted') | ||
232 | }) | ||
233 | }) | ||
234 | |||
235 | describe('Playlist hooks', function () { | ||
236 | let playlistId: number | ||
237 | let videoId: number | ||
238 | |||
239 | before(async function () { | ||
240 | { | ||
241 | const { id } = await servers[0].playlists.create({ | ||
242 | attributes: { | ||
243 | displayName: 'My playlist', | ||
244 | privacy: VideoPlaylistPrivacy.PRIVATE | ||
245 | } | ||
246 | }) | ||
247 | playlistId = id | ||
248 | } | ||
249 | |||
250 | { | ||
251 | const { id } = await servers[0].videos.upload({ attributes: { name: 'my super name' } }) | ||
252 | videoId = id | ||
253 | } | ||
254 | }) | ||
255 | |||
256 | it('Should run action:api.video-playlist-element.created', async function () { | ||
257 | await servers[0].playlists.addElement({ playlistId, attributes: { videoId } }) | ||
258 | |||
259 | await checkHook('action:api.video-playlist-element.created') | ||
260 | }) | ||
261 | }) | ||
262 | |||
263 | describe('Notification hook', function () { | ||
264 | |||
265 | it('Should run action:notifier.notification.created', async function () { | ||
266 | await checkHook('action:notifier.notification.created', false) | ||
267 | }) | ||
268 | }) | ||
269 | |||
270 | describe('Activity Pub hooks', function () { | ||
271 | let videoUUID: string | ||
272 | |||
273 | it('Should run action:activity-pub.remote-video.created', async function () { | ||
274 | this.timeout(30000) | ||
275 | |||
276 | const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) | ||
277 | videoUUID = uuid | ||
278 | |||
279 | await servers[0].servers.waitUntilLog('action:activity-pub.remote-video.created - AP remote video - video remote video') | ||
280 | }) | ||
281 | |||
282 | it('Should run action:activity-pub.remote-video.updated', async function () { | ||
283 | this.timeout(30000) | ||
284 | |||
285 | await servers[1].videos.update({ id: videoUUID, attributes: { name: 'remote video updated' } }) | ||
286 | |||
287 | await servers[0].servers.waitUntilLog( | ||
288 | 'action:activity-pub.remote-video.updated - AP remote video updated - video remote video updated', | ||
289 | 1, | ||
290 | false | ||
291 | ) | ||
292 | }) | ||
293 | }) | ||
294 | |||
295 | after(async function () { | ||
296 | await cleanupTests(servers) | ||
297 | }) | ||
298 | }) | ||
diff --git a/packages/tests/src/plugins/external-auth.ts b/packages/tests/src/plugins/external-auth.ts new file mode 100644 index 000000000..c7fe22185 --- /dev/null +++ b/packages/tests/src/plugins/external-auth.ts | |||
@@ -0,0 +1,436 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, HttpStatusCodeType, UserAdminFlag, UserRole } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | decodeQueryString, | ||
10 | PeerTubeServer, | ||
11 | PluginsCommand, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | async function loginExternal (options: { | ||
16 | server: PeerTubeServer | ||
17 | npmName: string | ||
18 | authName: string | ||
19 | username: string | ||
20 | query?: any | ||
21 | expectedStatus?: HttpStatusCodeType | ||
22 | expectedStatusStep2?: HttpStatusCodeType | ||
23 | }) { | ||
24 | const res = await options.server.plugins.getExternalAuth({ | ||
25 | npmName: options.npmName, | ||
26 | npmVersion: '0.0.1', | ||
27 | authName: options.authName, | ||
28 | query: options.query, | ||
29 | expectedStatus: options.expectedStatus || HttpStatusCode.FOUND_302 | ||
30 | }) | ||
31 | |||
32 | if (res.status !== HttpStatusCode.FOUND_302) return | ||
33 | |||
34 | const location = res.header.location | ||
35 | const { externalAuthToken } = decodeQueryString(location) | ||
36 | |||
37 | const resLogin = await options.server.login.loginUsingExternalToken({ | ||
38 | username: options.username, | ||
39 | externalAuthToken: externalAuthToken as string, | ||
40 | expectedStatus: options.expectedStatusStep2 | ||
41 | }) | ||
42 | |||
43 | return resLogin.body | ||
44 | } | ||
45 | |||
46 | describe('Test external auth plugins', function () { | ||
47 | let server: PeerTubeServer | ||
48 | |||
49 | let cyanAccessToken: string | ||
50 | let cyanRefreshToken: string | ||
51 | |||
52 | let kefkaAccessToken: string | ||
53 | let kefkaRefreshToken: string | ||
54 | let kefkaId: number | ||
55 | |||
56 | let externalAuthToken: string | ||
57 | |||
58 | before(async function () { | ||
59 | this.timeout(30000) | ||
60 | |||
61 | server = await createSingleServer(1, { | ||
62 | rates_limit: { | ||
63 | login: { | ||
64 | max: 30 | ||
65 | } | ||
66 | } | ||
67 | }) | ||
68 | |||
69 | await setAccessTokensToServers([ server ]) | ||
70 | |||
71 | for (const suffix of [ 'one', 'two', 'three' ]) { | ||
72 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-external-auth-' + suffix) }) | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | it('Should display the correct configuration', async function () { | ||
77 | const config = await server.config.getConfig() | ||
78 | |||
79 | const auths = config.plugin.registeredExternalAuths | ||
80 | expect(auths).to.have.lengthOf(9) | ||
81 | |||
82 | const auth2 = auths.find((a) => a.authName === 'external-auth-2') | ||
83 | expect(auth2).to.exist | ||
84 | expect(auth2.authDisplayName).to.equal('External Auth 2') | ||
85 | expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one') | ||
86 | }) | ||
87 | |||
88 | it('Should redirect for a Cyan login', async function () { | ||
89 | const res = await server.plugins.getExternalAuth({ | ||
90 | npmName: 'test-external-auth-one', | ||
91 | npmVersion: '0.0.1', | ||
92 | authName: 'external-auth-1', | ||
93 | query: { | ||
94 | username: 'cyan' | ||
95 | }, | ||
96 | expectedStatus: HttpStatusCode.FOUND_302 | ||
97 | }) | ||
98 | |||
99 | const location = res.header.location | ||
100 | expect(location.startsWith('/login?')).to.be.true | ||
101 | |||
102 | const searchParams = decodeQueryString(location) | ||
103 | |||
104 | expect(searchParams.externalAuthToken).to.exist | ||
105 | expect(searchParams.username).to.equal('cyan') | ||
106 | |||
107 | externalAuthToken = searchParams.externalAuthToken as string | ||
108 | }) | ||
109 | |||
110 | it('Should reject auto external login with a missing or invalid token', async function () { | ||
111 | const command = server.login | ||
112 | |||
113 | await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
114 | await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
115 | }) | ||
116 | |||
117 | it('Should reject auto external login with a missing or invalid username', async function () { | ||
118 | const command = server.login | ||
119 | |||
120 | await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
121 | await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
122 | }) | ||
123 | |||
124 | it('Should reject auto external login with an expired token', async function () { | ||
125 | this.timeout(15000) | ||
126 | |||
127 | await wait(5000) | ||
128 | |||
129 | await server.login.loginUsingExternalToken({ | ||
130 | username: 'cyan', | ||
131 | externalAuthToken, | ||
132 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
133 | }) | ||
134 | |||
135 | await server.servers.waitUntilLog('expired external auth token', 4) | ||
136 | }) | ||
137 | |||
138 | it('Should auto login Cyan, create the user and use the token', async function () { | ||
139 | { | ||
140 | const res = await loginExternal({ | ||
141 | server, | ||
142 | npmName: 'test-external-auth-one', | ||
143 | authName: 'external-auth-1', | ||
144 | query: { | ||
145 | username: 'cyan' | ||
146 | }, | ||
147 | username: 'cyan' | ||
148 | }) | ||
149 | |||
150 | cyanAccessToken = res.access_token | ||
151 | cyanRefreshToken = res.refresh_token | ||
152 | } | ||
153 | |||
154 | { | ||
155 | const body = await server.users.getMyInfo({ token: cyanAccessToken }) | ||
156 | expect(body.username).to.equal('cyan') | ||
157 | expect(body.account.displayName).to.equal('cyan') | ||
158 | expect(body.email).to.equal('cyan@example.com') | ||
159 | expect(body.role.id).to.equal(UserRole.USER) | ||
160 | expect(body.adminFlags).to.equal(UserAdminFlag.NONE) | ||
161 | expect(body.videoQuota).to.equal(5242880) | ||
162 | expect(body.videoQuotaDaily).to.equal(-1) | ||
163 | } | ||
164 | }) | ||
165 | |||
166 | it('Should auto login Kefka, create the user and use the token', async function () { | ||
167 | { | ||
168 | const res = await loginExternal({ | ||
169 | server, | ||
170 | npmName: 'test-external-auth-one', | ||
171 | authName: 'external-auth-2', | ||
172 | username: 'kefka' | ||
173 | }) | ||
174 | |||
175 | kefkaAccessToken = res.access_token | ||
176 | kefkaRefreshToken = res.refresh_token | ||
177 | } | ||
178 | |||
179 | { | ||
180 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | ||
181 | expect(body.username).to.equal('kefka') | ||
182 | expect(body.account.displayName).to.equal('Kefka Palazzo') | ||
183 | expect(body.email).to.equal('kefka@example.com') | ||
184 | expect(body.role.id).to.equal(UserRole.ADMINISTRATOR) | ||
185 | expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) | ||
186 | expect(body.videoQuota).to.equal(42000) | ||
187 | expect(body.videoQuotaDaily).to.equal(42100) | ||
188 | |||
189 | kefkaId = body.id | ||
190 | } | ||
191 | }) | ||
192 | |||
193 | it('Should refresh Cyan token, but not Kefka token', async function () { | ||
194 | { | ||
195 | const resRefresh = await server.login.refreshToken({ refreshToken: cyanRefreshToken }) | ||
196 | cyanAccessToken = resRefresh.body.access_token | ||
197 | cyanRefreshToken = resRefresh.body.refresh_token | ||
198 | |||
199 | const body = await server.users.getMyInfo({ token: cyanAccessToken }) | ||
200 | expect(body.username).to.equal('cyan') | ||
201 | } | ||
202 | |||
203 | { | ||
204 | await server.login.refreshToken({ refreshToken: kefkaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
205 | } | ||
206 | }) | ||
207 | |||
208 | it('Should update Cyan profile', async function () { | ||
209 | await server.users.updateMe({ | ||
210 | token: cyanAccessToken, | ||
211 | displayName: 'Cyan Garamonde', | ||
212 | description: 'Retainer to the king of Doma' | ||
213 | }) | ||
214 | |||
215 | const body = await server.users.getMyInfo({ token: cyanAccessToken }) | ||
216 | expect(body.account.displayName).to.equal('Cyan Garamonde') | ||
217 | expect(body.account.description).to.equal('Retainer to the king of Doma') | ||
218 | }) | ||
219 | |||
220 | it('Should logout Cyan', async function () { | ||
221 | await server.login.logout({ token: cyanAccessToken }) | ||
222 | }) | ||
223 | |||
224 | it('Should have logged out Cyan', async function () { | ||
225 | await server.servers.waitUntilLog('On logout cyan') | ||
226 | |||
227 | await server.users.getMyInfo({ token: cyanAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
228 | }) | ||
229 | |||
230 | it('Should login Cyan and keep the old existing profile', async function () { | ||
231 | { | ||
232 | const res = await loginExternal({ | ||
233 | server, | ||
234 | npmName: 'test-external-auth-one', | ||
235 | authName: 'external-auth-1', | ||
236 | query: { | ||
237 | username: 'cyan' | ||
238 | }, | ||
239 | username: 'cyan' | ||
240 | }) | ||
241 | |||
242 | cyanAccessToken = res.access_token | ||
243 | } | ||
244 | |||
245 | const body = await server.users.getMyInfo({ token: cyanAccessToken }) | ||
246 | expect(body.username).to.equal('cyan') | ||
247 | expect(body.account.displayName).to.equal('Cyan Garamonde') | ||
248 | expect(body.account.description).to.equal('Retainer to the king of Doma') | ||
249 | expect(body.role.id).to.equal(UserRole.USER) | ||
250 | }) | ||
251 | |||
252 | it('Should login Kefka and update the profile', async function () { | ||
253 | { | ||
254 | await server.users.update({ userId: kefkaId, videoQuota: 43000, videoQuotaDaily: 43100 }) | ||
255 | await server.users.updateMe({ token: kefkaAccessToken, displayName: 'kefka updated' }) | ||
256 | |||
257 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | ||
258 | expect(body.username).to.equal('kefka') | ||
259 | expect(body.account.displayName).to.equal('kefka updated') | ||
260 | expect(body.videoQuota).to.equal(43000) | ||
261 | expect(body.videoQuotaDaily).to.equal(43100) | ||
262 | } | ||
263 | |||
264 | { | ||
265 | const res = await loginExternal({ | ||
266 | server, | ||
267 | npmName: 'test-external-auth-one', | ||
268 | authName: 'external-auth-2', | ||
269 | username: 'kefka' | ||
270 | }) | ||
271 | |||
272 | kefkaAccessToken = res.access_token | ||
273 | kefkaRefreshToken = res.refresh_token | ||
274 | |||
275 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | ||
276 | expect(body.username).to.equal('kefka') | ||
277 | expect(body.account.displayName).to.equal('Kefka Palazzo') | ||
278 | expect(body.videoQuota).to.equal(42000) | ||
279 | expect(body.videoQuotaDaily).to.equal(43100) | ||
280 | } | ||
281 | }) | ||
282 | |||
283 | it('Should not update an external auth email', async function () { | ||
284 | await server.users.updateMe({ | ||
285 | token: cyanAccessToken, | ||
286 | email: 'toto@example.com', | ||
287 | currentPassword: 'toto', | ||
288 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
289 | }) | ||
290 | }) | ||
291 | |||
292 | it('Should reject token of Kefka by the plugin hook', async function () { | ||
293 | await wait(5000) | ||
294 | |||
295 | await server.users.getMyInfo({ token: kefkaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
296 | }) | ||
297 | |||
298 | it('Should unregister external-auth-2 and do not login existing Kefka', async function () { | ||
299 | await server.plugins.updateSettings({ | ||
300 | npmName: 'peertube-plugin-test-external-auth-one', | ||
301 | settings: { disableKefka: true } | ||
302 | }) | ||
303 | |||
304 | await server.login.login({ user: { username: 'kefka', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
305 | |||
306 | await loginExternal({ | ||
307 | server, | ||
308 | npmName: 'test-external-auth-one', | ||
309 | authName: 'external-auth-2', | ||
310 | query: { | ||
311 | username: 'kefka' | ||
312 | }, | ||
313 | username: 'kefka', | ||
314 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
315 | }) | ||
316 | }) | ||
317 | |||
318 | it('Should have disabled this auth', async function () { | ||
319 | const config = await server.config.getConfig() | ||
320 | |||
321 | const auths = config.plugin.registeredExternalAuths | ||
322 | expect(auths).to.have.lengthOf(8) | ||
323 | |||
324 | const auth1 = auths.find(a => a.authName === 'external-auth-2') | ||
325 | expect(auth1).to.not.exist | ||
326 | }) | ||
327 | |||
328 | it('Should uninstall the plugin one and do not login Cyan', async function () { | ||
329 | await server.plugins.uninstall({ npmName: 'peertube-plugin-test-external-auth-one' }) | ||
330 | |||
331 | await loginExternal({ | ||
332 | server, | ||
333 | npmName: 'test-external-auth-one', | ||
334 | authName: 'external-auth-1', | ||
335 | query: { | ||
336 | username: 'cyan' | ||
337 | }, | ||
338 | username: 'cyan', | ||
339 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
340 | }) | ||
341 | |||
342 | await server.login.login({ user: { username: 'cyan', password: null }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
343 | await server.login.login({ user: { username: 'cyan', password: '' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
344 | await server.login.login({ user: { username: 'cyan', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
345 | }) | ||
346 | |||
347 | it('Should not login kefka with another plugin', async function () { | ||
348 | await loginExternal({ | ||
349 | server, | ||
350 | npmName: 'test-external-auth-two', | ||
351 | authName: 'external-auth-4', | ||
352 | username: 'kefka2', | ||
353 | expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 | ||
354 | }) | ||
355 | |||
356 | await loginExternal({ | ||
357 | server, | ||
358 | npmName: 'test-external-auth-two', | ||
359 | authName: 'external-auth-4', | ||
360 | username: 'kefka', | ||
361 | expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 | ||
362 | }) | ||
363 | }) | ||
364 | |||
365 | it('Should not login an existing user email', async function () { | ||
366 | await server.users.create({ username: 'existing_user', password: 'super_password' }) | ||
367 | |||
368 | await loginExternal({ | ||
369 | server, | ||
370 | npmName: 'test-external-auth-two', | ||
371 | authName: 'external-auth-6', | ||
372 | username: 'existing_user', | ||
373 | expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 | ||
374 | }) | ||
375 | }) | ||
376 | |||
377 | it('Should be able to login an existing user username and channel', async function () { | ||
378 | await server.users.create({ username: 'existing_user2' }) | ||
379 | await server.users.create({ username: 'existing_user2-1_channel' }) | ||
380 | |||
381 | // Test twice to ensure we don't generate a username on every login | ||
382 | for (let i = 0; i < 2; i++) { | ||
383 | const res = await loginExternal({ | ||
384 | server, | ||
385 | npmName: 'test-external-auth-two', | ||
386 | authName: 'external-auth-7', | ||
387 | username: 'existing_user2' | ||
388 | }) | ||
389 | |||
390 | const token = res.access_token | ||
391 | |||
392 | const myInfo = await server.users.getMyInfo({ token }) | ||
393 | expect(myInfo.username).to.equal('existing_user2-1') | ||
394 | |||
395 | expect(myInfo.videoChannels[0].name).to.equal('existing_user2-1_channel-1') | ||
396 | } | ||
397 | }) | ||
398 | |||
399 | it('Should display the correct configuration', async function () { | ||
400 | const config = await server.config.getConfig() | ||
401 | |||
402 | const auths = config.plugin.registeredExternalAuths | ||
403 | expect(auths).to.have.lengthOf(7) | ||
404 | |||
405 | const auth2 = auths.find((a) => a.authName === 'external-auth-2') | ||
406 | expect(auth2).to.not.exist | ||
407 | }) | ||
408 | |||
409 | after(async function () { | ||
410 | await cleanupTests([ server ]) | ||
411 | }) | ||
412 | |||
413 | it('Should forward the redirectUrl if the plugin returns one', async function () { | ||
414 | const resLogin = await loginExternal({ | ||
415 | server, | ||
416 | npmName: 'test-external-auth-three', | ||
417 | authName: 'external-auth-7', | ||
418 | username: 'cid' | ||
419 | }) | ||
420 | |||
421 | const { redirectUrl } = await server.login.logout({ token: resLogin.access_token }) | ||
422 | expect(redirectUrl).to.equal('https://example.com/redirectUrl') | ||
423 | }) | ||
424 | |||
425 | it('Should call the plugin\'s onLogout method with the request', async function () { | ||
426 | const resLogin = await loginExternal({ | ||
427 | server, | ||
428 | npmName: 'test-external-auth-three', | ||
429 | authName: 'external-auth-8', | ||
430 | username: 'cid' | ||
431 | }) | ||
432 | |||
433 | const { redirectUrl } = await server.login.logout({ token: resLogin.access_token }) | ||
434 | expect(redirectUrl).to.equal('https://example.com/redirectUrl?access_token=' + resLogin.access_token) | ||
435 | }) | ||
436 | }) | ||
diff --git a/packages/tests/src/plugins/filter-hooks.ts b/packages/tests/src/plugins/filter-hooks.ts new file mode 100644 index 000000000..88cfee631 --- /dev/null +++ b/packages/tests/src/plugins/filter-hooks.ts | |||
@@ -0,0 +1,909 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | HttpStatusCode, | ||
6 | PeerTubeProblemDocument, | ||
7 | VideoDetails, | ||
8 | VideoImportState, | ||
9 | VideoPlaylist, | ||
10 | VideoPlaylistPrivacy, | ||
11 | VideoPrivacy | ||
12 | } from '@peertube/peertube-models' | ||
13 | import { | ||
14 | cleanupTests, | ||
15 | createMultipleServers, | ||
16 | doubleFollow, | ||
17 | makeActivityPubGetRequest, | ||
18 | makeGetRequest, | ||
19 | makeRawRequest, | ||
20 | PeerTubeServer, | ||
21 | PluginsCommand, | ||
22 | setAccessTokensToServers, | ||
23 | setDefaultVideoChannel, | ||
24 | waitJobs | ||
25 | } from '@peertube/peertube-server-commands' | ||
26 | import { FIXTURE_URLS } from '../shared/tests.js' | ||
27 | |||
28 | describe('Test plugin filter hooks', function () { | ||
29 | let servers: PeerTubeServer[] | ||
30 | let videoUUID: string | ||
31 | let threadId: number | ||
32 | let videoPlaylistUUID: string | ||
33 | |||
34 | before(async function () { | ||
35 | this.timeout(120000) | ||
36 | |||
37 | servers = await createMultipleServers(2) | ||
38 | await setAccessTokensToServers(servers) | ||
39 | await setDefaultVideoChannel(servers) | ||
40 | await doubleFollow(servers[0], servers[1]) | ||
41 | |||
42 | await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() }) | ||
43 | await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') }) | ||
44 | { | ||
45 | ({ uuid: videoPlaylistUUID } = await servers[0].playlists.create({ | ||
46 | attributes: { | ||
47 | displayName: 'my super playlist', | ||
48 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
49 | description: 'my super description', | ||
50 | videoChannelId: servers[0].store.channel.id | ||
51 | } | ||
52 | })) | ||
53 | } | ||
54 | |||
55 | for (let i = 0; i < 10; i++) { | ||
56 | const video = await servers[0].videos.upload({ attributes: { name: 'default video ' + i } }) | ||
57 | await servers[0].playlists.addElement({ playlistId: videoPlaylistUUID, attributes: { videoId: video.id } }) | ||
58 | } | ||
59 | |||
60 | const { data } = await servers[0].videos.list() | ||
61 | videoUUID = data[0].uuid | ||
62 | |||
63 | await servers[0].config.updateCustomSubConfig({ | ||
64 | newConfig: { | ||
65 | live: { enabled: true }, | ||
66 | signup: { enabled: true }, | ||
67 | videoFile: { | ||
68 | update: { | ||
69 | enabled: true | ||
70 | } | ||
71 | }, | ||
72 | import: { | ||
73 | videos: { | ||
74 | http: { enabled: true }, | ||
75 | torrent: { enabled: true } | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | }) | ||
80 | |||
81 | // Root subscribes to itself | ||
82 | await servers[0].subscriptions.add({ targetUri: 'root_channel@' + servers[0].host }) | ||
83 | }) | ||
84 | |||
85 | describe('Videos', function () { | ||
86 | |||
87 | it('Should run filter:api.videos.list.params', async function () { | ||
88 | const { data } = await servers[0].videos.list({ start: 0, count: 2 }) | ||
89 | |||
90 | // 2 plugins do +1 to the count parameter | ||
91 | expect(data).to.have.lengthOf(4) | ||
92 | }) | ||
93 | |||
94 | it('Should run filter:api.videos.list.result', async function () { | ||
95 | const { total } = await servers[0].videos.list({ start: 0, count: 0 }) | ||
96 | |||
97 | // Plugin do +1 to the total result | ||
98 | expect(total).to.equal(11) | ||
99 | }) | ||
100 | |||
101 | it('Should run filter:api.video-playlist.videos.list.params', async function () { | ||
102 | const { data } = await servers[0].playlists.listVideos({ | ||
103 | count: 2, | ||
104 | playlistId: videoPlaylistUUID | ||
105 | }) | ||
106 | |||
107 | // 1 plugin do +1 to the count parameter | ||
108 | expect(data).to.have.lengthOf(3) | ||
109 | }) | ||
110 | |||
111 | it('Should run filter:api.video-playlist.videos.list.result', async function () { | ||
112 | const { total } = await servers[0].playlists.listVideos({ | ||
113 | count: 0, | ||
114 | playlistId: videoPlaylistUUID | ||
115 | }) | ||
116 | |||
117 | // Plugin do +1 to the total result | ||
118 | expect(total).to.equal(11) | ||
119 | }) | ||
120 | |||
121 | it('Should run filter:api.accounts.videos.list.params', async function () { | ||
122 | const { data } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 }) | ||
123 | |||
124 | // 1 plugin do +1 to the count parameter | ||
125 | expect(data).to.have.lengthOf(3) | ||
126 | }) | ||
127 | |||
128 | it('Should run filter:api.accounts.videos.list.result', async function () { | ||
129 | const { total } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 }) | ||
130 | |||
131 | // Plugin do +2 to the total result | ||
132 | expect(total).to.equal(12) | ||
133 | }) | ||
134 | |||
135 | it('Should run filter:api.video-channels.videos.list.params', async function () { | ||
136 | const { data } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 }) | ||
137 | |||
138 | // 1 plugin do +3 to the count parameter | ||
139 | expect(data).to.have.lengthOf(5) | ||
140 | }) | ||
141 | |||
142 | it('Should run filter:api.video-channels.videos.list.result', async function () { | ||
143 | const { total } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 }) | ||
144 | |||
145 | // Plugin do +3 to the total result | ||
146 | expect(total).to.equal(13) | ||
147 | }) | ||
148 | |||
149 | it('Should run filter:api.user.me.videos.list.params', async function () { | ||
150 | const { data } = await servers[0].videos.listMyVideos({ start: 0, count: 2 }) | ||
151 | |||
152 | // 1 plugin do +4 to the count parameter | ||
153 | expect(data).to.have.lengthOf(6) | ||
154 | }) | ||
155 | |||
156 | it('Should run filter:api.user.me.videos.list.result', async function () { | ||
157 | const { total } = await servers[0].videos.listMyVideos({ start: 0, count: 2 }) | ||
158 | |||
159 | // Plugin do +4 to the total result | ||
160 | expect(total).to.equal(14) | ||
161 | }) | ||
162 | |||
163 | it('Should run filter:api.user.me.subscription-videos.list.params', async function () { | ||
164 | const { data } = await servers[0].videos.listMySubscriptionVideos({ start: 0, count: 2 }) | ||
165 | |||
166 | // 1 plugin do +1 to the count parameter | ||
167 | expect(data).to.have.lengthOf(3) | ||
168 | }) | ||
169 | |||
170 | it('Should run filter:api.user.me.subscription-videos.list.result', async function () { | ||
171 | const { total } = await servers[0].videos.listMySubscriptionVideos({ start: 0, count: 2 }) | ||
172 | |||
173 | // Plugin do +4 to the total result | ||
174 | expect(total).to.equal(14) | ||
175 | }) | ||
176 | |||
177 | it('Should run filter:api.video.get.result', async function () { | ||
178 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
179 | expect(video.name).to.contain('<3') | ||
180 | }) | ||
181 | }) | ||
182 | |||
183 | describe('Video/live/import accept', function () { | ||
184 | |||
185 | it('Should run filter:api.video.upload.accept.result', async function () { | ||
186 | const options = { attributes: { name: 'video with bad word' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 } | ||
187 | await servers[0].videos.upload({ mode: 'legacy', ...options }) | ||
188 | await servers[0].videos.upload({ mode: 'resumable', ...options }) | ||
189 | }) | ||
190 | |||
191 | it('Should run filter:api.video.update-file.accept.result', async function () { | ||
192 | const res = await servers[0].videos.replaceSourceFile({ | ||
193 | videoId: videoUUID, | ||
194 | fixture: 'video_short1.webm', | ||
195 | completedExpectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
196 | }) | ||
197 | |||
198 | expect((res as any)?.error).to.equal('no webm') | ||
199 | }) | ||
200 | |||
201 | it('Should run filter:api.live-video.create.accept.result', async function () { | ||
202 | const attributes = { | ||
203 | name: 'video with bad word', | ||
204 | privacy: VideoPrivacy.PUBLIC, | ||
205 | channelId: servers[0].store.channel.id | ||
206 | } | ||
207 | |||
208 | await servers[0].live.create({ fields: attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
209 | }) | ||
210 | |||
211 | it('Should run filter:api.video.pre-import-url.accept.result', async function () { | ||
212 | const attributes = { | ||
213 | name: 'normal title', | ||
214 | privacy: VideoPrivacy.PUBLIC, | ||
215 | channelId: servers[0].store.channel.id, | ||
216 | targetUrl: FIXTURE_URLS.goodVideo + 'bad' | ||
217 | } | ||
218 | await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
219 | }) | ||
220 | |||
221 | it('Should run filter:api.video.pre-import-torrent.accept.result', async function () { | ||
222 | const attributes = { | ||
223 | name: 'bad torrent', | ||
224 | privacy: VideoPrivacy.PUBLIC, | ||
225 | channelId: servers[0].store.channel.id, | ||
226 | torrentfile: 'video-720p.torrent' as any | ||
227 | } | ||
228 | await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
229 | }) | ||
230 | |||
231 | it('Should run filter:api.video.post-import-url.accept.result', async function () { | ||
232 | this.timeout(60000) | ||
233 | |||
234 | let videoImportId: number | ||
235 | |||
236 | { | ||
237 | const attributes = { | ||
238 | name: 'title with bad word', | ||
239 | privacy: VideoPrivacy.PUBLIC, | ||
240 | channelId: servers[0].store.channel.id, | ||
241 | targetUrl: FIXTURE_URLS.goodVideo | ||
242 | } | ||
243 | const body = await servers[0].imports.importVideo({ attributes }) | ||
244 | videoImportId = body.id | ||
245 | } | ||
246 | |||
247 | await waitJobs(servers) | ||
248 | |||
249 | { | ||
250 | const body = await servers[0].imports.getMyVideoImports() | ||
251 | const videoImports = body.data | ||
252 | |||
253 | const videoImport = videoImports.find(i => i.id === videoImportId) | ||
254 | |||
255 | expect(videoImport.state.id).to.equal(VideoImportState.REJECTED) | ||
256 | expect(videoImport.state.label).to.equal('Rejected') | ||
257 | } | ||
258 | }) | ||
259 | |||
260 | it('Should run filter:api.video.post-import-torrent.accept.result', async function () { | ||
261 | this.timeout(60000) | ||
262 | |||
263 | let videoImportId: number | ||
264 | |||
265 | { | ||
266 | const attributes = { | ||
267 | name: 'title with bad word', | ||
268 | privacy: VideoPrivacy.PUBLIC, | ||
269 | channelId: servers[0].store.channel.id, | ||
270 | torrentfile: 'video-720p.torrent' as any | ||
271 | } | ||
272 | const body = await servers[0].imports.importVideo({ attributes }) | ||
273 | videoImportId = body.id | ||
274 | } | ||
275 | |||
276 | await waitJobs(servers) | ||
277 | |||
278 | { | ||
279 | const { data: videoImports } = await servers[0].imports.getMyVideoImports() | ||
280 | |||
281 | const videoImport = videoImports.find(i => i.id === videoImportId) | ||
282 | |||
283 | expect(videoImport.state.id).to.equal(VideoImportState.REJECTED) | ||
284 | expect(videoImport.state.label).to.equal('Rejected') | ||
285 | } | ||
286 | }) | ||
287 | }) | ||
288 | |||
289 | describe('Video comments accept', function () { | ||
290 | |||
291 | it('Should run filter:api.video-thread.create.accept.result', async function () { | ||
292 | await servers[0].comments.createThread({ | ||
293 | videoId: videoUUID, | ||
294 | text: 'comment with bad word', | ||
295 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
296 | }) | ||
297 | }) | ||
298 | |||
299 | it('Should run filter:api.video-comment-reply.create.accept.result', async function () { | ||
300 | const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' }) | ||
301 | threadId = created.id | ||
302 | |||
303 | await servers[0].comments.addReply({ | ||
304 | videoId: videoUUID, | ||
305 | toCommentId: threadId, | ||
306 | text: 'comment with bad word', | ||
307 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
308 | }) | ||
309 | await servers[0].comments.addReply({ | ||
310 | videoId: videoUUID, | ||
311 | toCommentId: threadId, | ||
312 | text: 'comment with good word', | ||
313 | expectedStatus: HttpStatusCode.OK_200 | ||
314 | }) | ||
315 | }) | ||
316 | |||
317 | it('Should run filter:activity-pub.remote-video-comment.create.accept.result on a thread creation', async function () { | ||
318 | this.timeout(30000) | ||
319 | |||
320 | await servers[1].comments.createThread({ videoId: videoUUID, text: 'comment with bad word' }) | ||
321 | |||
322 | await waitJobs(servers) | ||
323 | |||
324 | { | ||
325 | const thread = await servers[0].comments.listThreads({ videoId: videoUUID }) | ||
326 | expect(thread.data).to.have.lengthOf(1) | ||
327 | expect(thread.data[0].text).to.not.include(' bad ') | ||
328 | } | ||
329 | |||
330 | { | ||
331 | const thread = await servers[1].comments.listThreads({ videoId: videoUUID }) | ||
332 | expect(thread.data).to.have.lengthOf(2) | ||
333 | } | ||
334 | }) | ||
335 | |||
336 | it('Should run filter:activity-pub.remote-video-comment.create.accept.result on a reply creation', async function () { | ||
337 | this.timeout(30000) | ||
338 | |||
339 | const { data } = await servers[1].comments.listThreads({ videoId: videoUUID }) | ||
340 | const threadIdServer2 = data.find(t => t.text === 'thread').id | ||
341 | |||
342 | await servers[1].comments.addReply({ | ||
343 | videoId: videoUUID, | ||
344 | toCommentId: threadIdServer2, | ||
345 | text: 'comment with bad word' | ||
346 | }) | ||
347 | |||
348 | await waitJobs(servers) | ||
349 | |||
350 | { | ||
351 | const tree = await servers[0].comments.getThread({ videoId: videoUUID, threadId }) | ||
352 | expect(tree.children).to.have.lengthOf(1) | ||
353 | expect(tree.children[0].comment.text).to.not.include(' bad ') | ||
354 | } | ||
355 | |||
356 | { | ||
357 | const tree = await servers[1].comments.getThread({ videoId: videoUUID, threadId: threadIdServer2 }) | ||
358 | expect(tree.children).to.have.lengthOf(2) | ||
359 | } | ||
360 | }) | ||
361 | }) | ||
362 | |||
363 | describe('Video comments', function () { | ||
364 | |||
365 | it('Should run filter:api.video-threads.list.params', async function () { | ||
366 | const { data } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 }) | ||
367 | |||
368 | // our plugin do +1 to the count parameter | ||
369 | expect(data).to.have.lengthOf(1) | ||
370 | }) | ||
371 | |||
372 | it('Should run filter:api.video-threads.list.result', async function () { | ||
373 | const { total } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 }) | ||
374 | |||
375 | // Plugin do +1 to the total result | ||
376 | expect(total).to.equal(2) | ||
377 | }) | ||
378 | |||
379 | it('Should run filter:api.video-thread-comments.list.params') | ||
380 | |||
381 | it('Should run filter:api.video-thread-comments.list.result', async function () { | ||
382 | const thread = await servers[0].comments.getThread({ videoId: videoUUID, threadId }) | ||
383 | |||
384 | expect(thread.comment.text.endsWith(' <3')).to.be.true | ||
385 | }) | ||
386 | |||
387 | it('Should run filter:api.overviews.videos.list.{params,result}', async function () { | ||
388 | await servers[0].overviews.getVideos({ page: 1 }) | ||
389 | |||
390 | // 3 because we get 3 samples per page | ||
391 | await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.params', 3) | ||
392 | await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.result', 3) | ||
393 | }) | ||
394 | }) | ||
395 | |||
396 | describe('filter:video.auto-blacklist.result', function () { | ||
397 | |||
398 | async function checkIsBlacklisted (id: number | string, value: boolean) { | ||
399 | const video = await servers[0].videos.getWithToken({ id }) | ||
400 | expect(video.blacklisted).to.equal(value) | ||
401 | } | ||
402 | |||
403 | it('Should blacklist on upload', async function () { | ||
404 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video please blacklist me' } }) | ||
405 | await checkIsBlacklisted(uuid, true) | ||
406 | }) | ||
407 | |||
408 | it('Should blacklist on import', async function () { | ||
409 | this.timeout(15000) | ||
410 | |||
411 | const attributes = { | ||
412 | name: 'video please blacklist me', | ||
413 | targetUrl: FIXTURE_URLS.goodVideo, | ||
414 | channelId: servers[0].store.channel.id | ||
415 | } | ||
416 | const body = await servers[0].imports.importVideo({ attributes }) | ||
417 | await checkIsBlacklisted(body.video.uuid, true) | ||
418 | }) | ||
419 | |||
420 | it('Should blacklist on update', async function () { | ||
421 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' } }) | ||
422 | await checkIsBlacklisted(uuid, false) | ||
423 | |||
424 | await servers[0].videos.update({ id: uuid, attributes: { name: 'please blacklist me' } }) | ||
425 | await checkIsBlacklisted(uuid, true) | ||
426 | }) | ||
427 | |||
428 | it('Should blacklist on remote upload', async function () { | ||
429 | this.timeout(120000) | ||
430 | |||
431 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'remote please blacklist me' } }) | ||
432 | await waitJobs(servers) | ||
433 | |||
434 | await checkIsBlacklisted(uuid, true) | ||
435 | }) | ||
436 | |||
437 | it('Should blacklist on remote update', async function () { | ||
438 | this.timeout(120000) | ||
439 | |||
440 | const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video' } }) | ||
441 | await waitJobs(servers) | ||
442 | |||
443 | await checkIsBlacklisted(uuid, false) | ||
444 | |||
445 | await servers[1].videos.update({ id: uuid, attributes: { name: 'please blacklist me' } }) | ||
446 | await waitJobs(servers) | ||
447 | |||
448 | await checkIsBlacklisted(uuid, true) | ||
449 | }) | ||
450 | }) | ||
451 | |||
452 | describe('Should run filter:api.user.signup.allowed.result', function () { | ||
453 | |||
454 | before(async function () { | ||
455 | await servers[0].config.updateExistingSubConfig({ newConfig: { signup: { requiresApproval: false } } }) | ||
456 | }) | ||
457 | |||
458 | it('Should run on config endpoint', async function () { | ||
459 | const body = await servers[0].config.getConfig() | ||
460 | expect(body.signup.allowed).to.be.true | ||
461 | }) | ||
462 | |||
463 | it('Should allow a signup', async function () { | ||
464 | await servers[0].registrations.register({ username: 'john1' }) | ||
465 | }) | ||
466 | |||
467 | it('Should not allow a signup', async function () { | ||
468 | const res = await servers[0].registrations.register({ | ||
469 | username: 'jma 1', | ||
470 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
471 | }) | ||
472 | |||
473 | expect(res.body.error).to.equal('No jma 1') | ||
474 | }) | ||
475 | }) | ||
476 | |||
477 | describe('Should run filter:api.user.request-signup.allowed.result', function () { | ||
478 | |||
479 | before(async function () { | ||
480 | await servers[0].config.updateExistingSubConfig({ newConfig: { signup: { requiresApproval: true } } }) | ||
481 | }) | ||
482 | |||
483 | it('Should run on config endpoint', async function () { | ||
484 | const body = await servers[0].config.getConfig() | ||
485 | expect(body.signup.allowed).to.be.true | ||
486 | }) | ||
487 | |||
488 | it('Should allow a signup request', async function () { | ||
489 | await servers[0].registrations.requestRegistration({ username: 'john2', registrationReason: 'tt' }) | ||
490 | }) | ||
491 | |||
492 | it('Should not allow a signup request', async function () { | ||
493 | const body = await servers[0].registrations.requestRegistration({ | ||
494 | username: 'jma 2', | ||
495 | registrationReason: 'tt', | ||
496 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
497 | }) | ||
498 | |||
499 | expect((body as unknown as PeerTubeProblemDocument).error).to.equal('No jma 2') | ||
500 | }) | ||
501 | }) | ||
502 | |||
503 | describe('Download hooks', function () { | ||
504 | const downloadVideos: VideoDetails[] = [] | ||
505 | let downloadVideo2Token: string | ||
506 | |||
507 | before(async function () { | ||
508 | this.timeout(120000) | ||
509 | |||
510 | await servers[0].config.updateCustomSubConfig({ | ||
511 | newConfig: { | ||
512 | transcoding: { | ||
513 | webVideos: { | ||
514 | enabled: true | ||
515 | }, | ||
516 | hls: { | ||
517 | enabled: true | ||
518 | } | ||
519 | } | ||
520 | } | ||
521 | }) | ||
522 | |||
523 | const uuids: string[] = [] | ||
524 | |||
525 | for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) { | ||
526 | const uuid = (await servers[0].videos.quickUpload({ name })).uuid | ||
527 | uuids.push(uuid) | ||
528 | } | ||
529 | |||
530 | await waitJobs(servers) | ||
531 | |||
532 | for (const uuid of uuids) { | ||
533 | downloadVideos.push(await servers[0].videos.get({ id: uuid })) | ||
534 | } | ||
535 | |||
536 | downloadVideo2Token = await servers[0].videoToken.getVideoFileToken({ videoId: downloadVideos[2].uuid }) | ||
537 | }) | ||
538 | |||
539 | it('Should run filter:api.download.torrent.allowed.result', async function () { | ||
540 | const res = await makeRawRequest({ url: downloadVideos[0].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
541 | expect(res.body.error).to.equal('Liu Bei') | ||
542 | |||
543 | await makeRawRequest({ url: downloadVideos[1].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
544 | await makeRawRequest({ url: downloadVideos[2].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) | ||
545 | }) | ||
546 | |||
547 | it('Should run filter:api.download.video.allowed.result', async function () { | ||
548 | { | ||
549 | const refused = downloadVideos[1].files[0].fileDownloadUrl | ||
550 | const allowed = [ | ||
551 | downloadVideos[0].files[0].fileDownloadUrl, | ||
552 | downloadVideos[2].files[0].fileDownloadUrl | ||
553 | ] | ||
554 | |||
555 | const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
556 | expect(res.body.error).to.equal('Cao Cao') | ||
557 | |||
558 | for (const url of allowed) { | ||
559 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) | ||
560 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) | ||
561 | } | ||
562 | } | ||
563 | |||
564 | { | ||
565 | const refused = downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl | ||
566 | |||
567 | const allowed = [ | ||
568 | downloadVideos[2].files[0].fileDownloadUrl, | ||
569 | downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, | ||
570 | downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl | ||
571 | ] | ||
572 | |||
573 | // Only streaming playlist is refuse | ||
574 | const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
575 | expect(res.body.error).to.equal('Sun Jian') | ||
576 | |||
577 | // But not we there is a user in res | ||
578 | await makeRawRequest({ url: refused, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 }) | ||
579 | await makeRawRequest({ url: refused, query: { videoFileToken: downloadVideo2Token }, expectedStatus: HttpStatusCode.OK_200 }) | ||
580 | |||
581 | // Other files work | ||
582 | for (const url of allowed) { | ||
583 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) | ||
584 | } | ||
585 | } | ||
586 | }) | ||
587 | }) | ||
588 | |||
589 | describe('Embed filters', function () { | ||
590 | const embedVideos: VideoDetails[] = [] | ||
591 | const embedPlaylists: VideoPlaylist[] = [] | ||
592 | |||
593 | before(async function () { | ||
594 | this.timeout(60000) | ||
595 | |||
596 | await servers[0].config.disableTranscoding() | ||
597 | |||
598 | for (const name of [ 'bad embed', 'good embed' ]) { | ||
599 | { | ||
600 | const uuid = (await servers[0].videos.quickUpload({ name })).uuid | ||
601 | embedVideos.push(await servers[0].videos.get({ id: uuid })) | ||
602 | } | ||
603 | |||
604 | { | ||
605 | const attributes = { displayName: name, videoChannelId: servers[0].store.channel.id, privacy: VideoPlaylistPrivacy.PUBLIC } | ||
606 | const { id } = await servers[0].playlists.create({ attributes }) | ||
607 | |||
608 | const playlist = await servers[0].playlists.get({ playlistId: id }) | ||
609 | embedPlaylists.push(playlist) | ||
610 | } | ||
611 | } | ||
612 | }) | ||
613 | |||
614 | it('Should run filter:html.embed.video.allowed.result', async function () { | ||
615 | const res = await makeGetRequest({ url: servers[0].url, path: embedVideos[0].embedPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
616 | expect(res.text).to.equal('Lu Bu') | ||
617 | }) | ||
618 | |||
619 | it('Should run filter:html.embed.video-playlist.allowed.result', async function () { | ||
620 | const res = await makeGetRequest({ url: servers[0].url, path: embedPlaylists[0].embedPath, expectedStatus: HttpStatusCode.OK_200 }) | ||
621 | expect(res.text).to.equal('Diao Chan') | ||
622 | }) | ||
623 | }) | ||
624 | |||
625 | describe('Client HTML filters', function () { | ||
626 | let videoUUID: string | ||
627 | |||
628 | before(async function () { | ||
629 | this.timeout(60000) | ||
630 | |||
631 | const { uuid } = await servers[0].videos.quickUpload({ name: 'html video' }) | ||
632 | videoUUID = uuid | ||
633 | }) | ||
634 | |||
635 | it('Should run filter:html.client.json-ld.result', async function () { | ||
636 | const res = await makeGetRequest({ url: servers[0].url, path: '/w/' + videoUUID, expectedStatus: HttpStatusCode.OK_200 }) | ||
637 | expect(res.text).to.contain('"recordedAt":"http://example.com/recordedAt"') | ||
638 | }) | ||
639 | |||
640 | it('Should not run filter:html.client.json-ld.result with an account', async function () { | ||
641 | const res = await makeGetRequest({ url: servers[0].url, path: '/a/root', expectedStatus: HttpStatusCode.OK_200 }) | ||
642 | expect(res.text).not.to.contain('"recordedAt":"http://example.com/recordedAt"') | ||
643 | }) | ||
644 | }) | ||
645 | |||
646 | describe('Search filters', function () { | ||
647 | |||
648 | before(async function () { | ||
649 | await servers[0].config.updateCustomSubConfig({ | ||
650 | newConfig: { | ||
651 | search: { | ||
652 | searchIndex: { | ||
653 | enabled: true, | ||
654 | isDefaultSearch: false, | ||
655 | disableLocalSearch: false | ||
656 | } | ||
657 | } | ||
658 | } | ||
659 | }) | ||
660 | }) | ||
661 | |||
662 | it('Should run filter:api.search.videos.local.list.{params,result}', async function () { | ||
663 | await servers[0].search.advancedVideoSearch({ | ||
664 | search: { | ||
665 | search: 'Sun Quan' | ||
666 | } | ||
667 | }) | ||
668 | |||
669 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.params', 1) | ||
670 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.result', 1) | ||
671 | }) | ||
672 | |||
673 | it('Should run filter:api.search.videos.index.list.{params,result}', async function () { | ||
674 | await servers[0].search.advancedVideoSearch({ | ||
675 | search: { | ||
676 | search: 'Sun Quan', | ||
677 | searchTarget: 'search-index' | ||
678 | } | ||
679 | }) | ||
680 | |||
681 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.params', 1) | ||
682 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.result', 1) | ||
683 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.index.list.params', 1) | ||
684 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.index.list.result', 1) | ||
685 | }) | ||
686 | |||
687 | it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () { | ||
688 | await servers[0].search.advancedChannelSearch({ | ||
689 | search: { | ||
690 | search: 'Sun Ce' | ||
691 | } | ||
692 | }) | ||
693 | |||
694 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.params', 1) | ||
695 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.result', 1) | ||
696 | }) | ||
697 | |||
698 | it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () { | ||
699 | await servers[0].search.advancedChannelSearch({ | ||
700 | search: { | ||
701 | search: 'Sun Ce', | ||
702 | searchTarget: 'search-index' | ||
703 | } | ||
704 | }) | ||
705 | |||
706 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.params', 1) | ||
707 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.result', 1) | ||
708 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.index.list.params', 1) | ||
709 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.index.list.result', 1) | ||
710 | }) | ||
711 | |||
712 | it('Should run filter:api.search.video-playlists.local.list.{params,result}', async function () { | ||
713 | await servers[0].search.advancedPlaylistSearch({ | ||
714 | search: { | ||
715 | search: 'Sun Jian' | ||
716 | } | ||
717 | }) | ||
718 | |||
719 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.params', 1) | ||
720 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.result', 1) | ||
721 | }) | ||
722 | |||
723 | it('Should run filter:api.search.video-playlists.index.list.{params,result}', async function () { | ||
724 | await servers[0].search.advancedPlaylistSearch({ | ||
725 | search: { | ||
726 | search: 'Sun Jian', | ||
727 | searchTarget: 'search-index' | ||
728 | } | ||
729 | }) | ||
730 | |||
731 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.params', 1) | ||
732 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.result', 1) | ||
733 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.index.list.params', 1) | ||
734 | await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.index.list.result', 1) | ||
735 | }) | ||
736 | }) | ||
737 | |||
738 | describe('Upload/import/live attributes filters', function () { | ||
739 | |||
740 | before(async function () { | ||
741 | await servers[0].config.enableLive({ transcoding: false, allowReplay: false }) | ||
742 | await servers[0].config.enableImports() | ||
743 | await servers[0].config.disableTranscoding() | ||
744 | }) | ||
745 | |||
746 | it('Should run filter:api.video.upload.video-attribute.result', async function () { | ||
747 | for (const mode of [ 'legacy' as 'legacy', 'resumable' as 'resumable' ]) { | ||
748 | const { id } = await servers[0].videos.upload({ attributes: { name: 'video', description: 'upload' }, mode }) | ||
749 | |||
750 | const video = await servers[0].videos.get({ id }) | ||
751 | expect(video.description).to.equal('upload - filter:api.video.upload.video-attribute.result') | ||
752 | } | ||
753 | }) | ||
754 | |||
755 | it('Should run filter:api.video.import-url.video-attribute.result', async function () { | ||
756 | const attributes = { | ||
757 | name: 'video', | ||
758 | description: 'import url', | ||
759 | channelId: servers[0].store.channel.id, | ||
760 | targetUrl: FIXTURE_URLS.goodVideo, | ||
761 | privacy: VideoPrivacy.PUBLIC | ||
762 | } | ||
763 | const { video: { id } } = await servers[0].imports.importVideo({ attributes }) | ||
764 | |||
765 | const video = await servers[0].videos.get({ id }) | ||
766 | expect(video.description).to.equal('import url - filter:api.video.import-url.video-attribute.result') | ||
767 | }) | ||
768 | |||
769 | it('Should run filter:api.video.import-torrent.video-attribute.result', async function () { | ||
770 | const attributes = { | ||
771 | name: 'video', | ||
772 | description: 'import torrent', | ||
773 | channelId: servers[0].store.channel.id, | ||
774 | magnetUri: FIXTURE_URLS.magnet, | ||
775 | privacy: VideoPrivacy.PUBLIC | ||
776 | } | ||
777 | const { video: { id } } = await servers[0].imports.importVideo({ attributes }) | ||
778 | |||
779 | const video = await servers[0].videos.get({ id }) | ||
780 | expect(video.description).to.equal('import torrent - filter:api.video.import-torrent.video-attribute.result') | ||
781 | }) | ||
782 | |||
783 | it('Should run filter:api.video.live.video-attribute.result', async function () { | ||
784 | const fields = { | ||
785 | name: 'live', | ||
786 | description: 'live', | ||
787 | channelId: servers[0].store.channel.id, | ||
788 | privacy: VideoPrivacy.PUBLIC | ||
789 | } | ||
790 | const { id } = await servers[0].live.create({ fields }) | ||
791 | |||
792 | const video = await servers[0].videos.get({ id }) | ||
793 | expect(video.description).to.equal('live - filter:api.video.live.video-attribute.result') | ||
794 | }) | ||
795 | }) | ||
796 | |||
797 | describe('Stats filters', function () { | ||
798 | |||
799 | it('Should run filter:api.server.stats.get.result', async function () { | ||
800 | const data = await servers[0].stats.get() | ||
801 | |||
802 | expect((data as any).customStats).to.equal(14) | ||
803 | }) | ||
804 | |||
805 | }) | ||
806 | |||
807 | describe('Job queue filters', function () { | ||
808 | let videoUUID: string | ||
809 | |||
810 | before(async function () { | ||
811 | this.timeout(120_000) | ||
812 | |||
813 | await servers[0].config.enableMinimumTranscoding() | ||
814 | const { uuid } = await servers[0].videos.quickUpload({ name: 'studio' }) | ||
815 | |||
816 | const video = await servers[0].videos.get({ id: uuid }) | ||
817 | expect(video.duration).at.least(2) | ||
818 | videoUUID = video.uuid | ||
819 | |||
820 | await waitJobs(servers) | ||
821 | |||
822 | await servers[0].config.enableStudio() | ||
823 | }) | ||
824 | |||
825 | it('Should run filter:job-queue.process.params', async function () { | ||
826 | this.timeout(120_000) | ||
827 | |||
828 | await servers[0].videoStudio.createEditionTasks({ | ||
829 | videoId: videoUUID, | ||
830 | tasks: [ | ||
831 | { | ||
832 | name: 'add-intro', | ||
833 | options: { | ||
834 | file: 'video_very_short_240p.mp4' | ||
835 | } | ||
836 | } | ||
837 | ] | ||
838 | }) | ||
839 | |||
840 | await waitJobs(servers) | ||
841 | |||
842 | await servers[0].servers.waitUntilLog('Run hook filter:job-queue.process.params', 1, false) | ||
843 | |||
844 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
845 | expect(video.duration).at.most(2) | ||
846 | }) | ||
847 | |||
848 | it('Should run filter:job-queue.process.result', async function () { | ||
849 | await servers[0].servers.waitUntilLog('Run hook filter:job-queue.process.result', 1, false) | ||
850 | }) | ||
851 | }) | ||
852 | |||
853 | describe('Transcoding filters', async function () { | ||
854 | |||
855 | it('Should run filter:transcoding.auto.resolutions-to-transcode.result', async function () { | ||
856 | const { uuid } = await servers[0].videos.quickUpload({ name: 'transcode-filter' }) | ||
857 | |||
858 | await waitJobs(servers) | ||
859 | |||
860 | const video = await servers[0].videos.get({ id: uuid }) | ||
861 | expect(video.files).to.have.lengthOf(2) | ||
862 | expect(video.files.find(f => f.resolution.id === 100 as any)).to.exist | ||
863 | }) | ||
864 | }) | ||
865 | |||
866 | describe('Video channel filters', async function () { | ||
867 | |||
868 | it('Should run filter:api.video-channels.list.params', async function () { | ||
869 | const { data } = await servers[0].channels.list({ start: 0, count: 0 }) | ||
870 | |||
871 | // plugin do +1 to the count parameter | ||
872 | expect(data).to.have.lengthOf(1) | ||
873 | }) | ||
874 | |||
875 | it('Should run filter:api.video-channels.list.result', async function () { | ||
876 | const { total } = await servers[0].channels.list({ start: 0, count: 1 }) | ||
877 | |||
878 | // plugin do +1 to the total parameter | ||
879 | expect(total).to.equal(4) | ||
880 | }) | ||
881 | |||
882 | it('Should run filter:api.video-channel.get.result', async function () { | ||
883 | const channel = await servers[0].channels.get({ channelName: 'root_channel' }) | ||
884 | expect(channel.displayName).to.equal('Main root channel <3') | ||
885 | }) | ||
886 | }) | ||
887 | |||
888 | describe('Activity Pub', function () { | ||
889 | |||
890 | it('Should run filter:activity-pub.activity.context.build.result', async function () { | ||
891 | const { body } = await makeActivityPubGetRequest(servers[0].url, '/w/' + videoUUID) | ||
892 | expect(body.type).to.equal('Video') | ||
893 | |||
894 | expect(body['@context'].some(c => { | ||
895 | return typeof c === 'object' && c.recordedAt === 'https://schema.org/recordedAt' | ||
896 | })).to.be.true | ||
897 | }) | ||
898 | |||
899 | it('Should run filter:activity-pub.video.json-ld.build.result', async function () { | ||
900 | const { body } = await makeActivityPubGetRequest(servers[0].url, '/w/' + videoUUID) | ||
901 | expect(body.name).to.equal('default video 0') | ||
902 | expect(body.videoName).to.equal('default video 0') | ||
903 | }) | ||
904 | }) | ||
905 | |||
906 | after(async function () { | ||
907 | await cleanupTests(servers) | ||
908 | }) | ||
909 | }) | ||
diff --git a/packages/tests/src/plugins/html-injection.ts b/packages/tests/src/plugins/html-injection.ts new file mode 100644 index 000000000..269a45b98 --- /dev/null +++ b/packages/tests/src/plugins/html-injection.ts | |||
@@ -0,0 +1,73 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makeHTMLRequest, | ||
8 | PeerTubeServer, | ||
9 | PluginsCommand, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('Test plugins HTML injection', function () { | ||
14 | let server: PeerTubeServer = null | ||
15 | let command: PluginsCommand | ||
16 | |||
17 | before(async function () { | ||
18 | this.timeout(30000) | ||
19 | |||
20 | server = await createSingleServer(1) | ||
21 | await setAccessTokensToServers([ server ]) | ||
22 | |||
23 | command = server.plugins | ||
24 | }) | ||
25 | |||
26 | it('Should not inject global css file in HTML', async function () { | ||
27 | { | ||
28 | const text = await command.getCSS() | ||
29 | expect(text).to.be.empty | ||
30 | } | ||
31 | |||
32 | for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) { | ||
33 | const res = await makeHTMLRequest(server.url, path) | ||
34 | expect(res.text).to.not.include('link rel="stylesheet" href="/plugins/global.css') | ||
35 | } | ||
36 | }) | ||
37 | |||
38 | it('Should install a plugin and a theme', async function () { | ||
39 | this.timeout(30000) | ||
40 | |||
41 | await command.install({ npmName: 'peertube-plugin-hello-world' }) | ||
42 | }) | ||
43 | |||
44 | it('Should have the correct global css', async function () { | ||
45 | { | ||
46 | const text = await command.getCSS() | ||
47 | expect(text).to.contain('background-color: red') | ||
48 | } | ||
49 | |||
50 | for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) { | ||
51 | const res = await makeHTMLRequest(server.url, path) | ||
52 | expect(res.text).to.include('link rel="stylesheet" href="/plugins/global.css') | ||
53 | } | ||
54 | }) | ||
55 | |||
56 | it('Should have an empty global css on uninstall', async function () { | ||
57 | await command.uninstall({ npmName: 'peertube-plugin-hello-world' }) | ||
58 | |||
59 | { | ||
60 | const text = await command.getCSS() | ||
61 | expect(text).to.be.empty | ||
62 | } | ||
63 | |||
64 | for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) { | ||
65 | const res = await makeHTMLRequest(server.url, path) | ||
66 | expect(res.text).to.not.include('link rel="stylesheet" href="/plugins/global.css') | ||
67 | } | ||
68 | }) | ||
69 | |||
70 | after(async function () { | ||
71 | await cleanupTests([ server ]) | ||
72 | }) | ||
73 | }) | ||
diff --git a/packages/tests/src/plugins/id-and-pass-auth.ts b/packages/tests/src/plugins/id-and-pass-auth.ts new file mode 100644 index 000000000..a332f0eec --- /dev/null +++ b/packages/tests/src/plugins/id-and-pass-auth.ts | |||
@@ -0,0 +1,248 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, UserRole } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | PluginsCommand, | ||
11 | setAccessTokensToServers | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('Test id and pass auth plugins', function () { | ||
15 | let server: PeerTubeServer | ||
16 | |||
17 | let crashAccessToken: string | ||
18 | let crashRefreshToken: string | ||
19 | |||
20 | let lagunaAccessToken: string | ||
21 | let lagunaRefreshToken: string | ||
22 | let lagunaId: number | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await createSingleServer(1) | ||
28 | await setAccessTokensToServers([ server ]) | ||
29 | |||
30 | for (const suffix of [ 'one', 'two', 'three' ]) { | ||
31 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-id-pass-auth-' + suffix) }) | ||
32 | } | ||
33 | }) | ||
34 | |||
35 | it('Should display the correct configuration', async function () { | ||
36 | const config = await server.config.getConfig() | ||
37 | |||
38 | const auths = config.plugin.registeredIdAndPassAuths | ||
39 | expect(auths).to.have.lengthOf(8) | ||
40 | |||
41 | const crashAuth = auths.find(a => a.authName === 'crash-auth') | ||
42 | expect(crashAuth).to.exist | ||
43 | expect(crashAuth.npmName).to.equal('peertube-plugin-test-id-pass-auth-one') | ||
44 | expect(crashAuth.weight).to.equal(50) | ||
45 | }) | ||
46 | |||
47 | it('Should not login', async function () { | ||
48 | await server.login.login({ user: { username: 'toto', password: 'password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
49 | }) | ||
50 | |||
51 | it('Should login Spyro, create the user and use the token', async function () { | ||
52 | const accessToken = await server.login.getAccessToken({ username: 'spyro', password: 'spyro password' }) | ||
53 | |||
54 | const body = await server.users.getMyInfo({ token: accessToken }) | ||
55 | |||
56 | expect(body.username).to.equal('spyro') | ||
57 | expect(body.account.displayName).to.equal('Spyro the Dragon') | ||
58 | expect(body.role.id).to.equal(UserRole.USER) | ||
59 | }) | ||
60 | |||
61 | it('Should login Crash, create the user and use the token', async function () { | ||
62 | { | ||
63 | const body = await server.login.login({ user: { username: 'crash', password: 'crash password' } }) | ||
64 | crashAccessToken = body.access_token | ||
65 | crashRefreshToken = body.refresh_token | ||
66 | } | ||
67 | |||
68 | { | ||
69 | const body = await server.users.getMyInfo({ token: crashAccessToken }) | ||
70 | |||
71 | expect(body.username).to.equal('crash') | ||
72 | expect(body.account.displayName).to.equal('Crash Bandicoot') | ||
73 | expect(body.role.id).to.equal(UserRole.MODERATOR) | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | it('Should login the first Laguna, create the user and use the token', async function () { | ||
78 | { | ||
79 | const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } }) | ||
80 | lagunaAccessToken = body.access_token | ||
81 | lagunaRefreshToken = body.refresh_token | ||
82 | } | ||
83 | |||
84 | { | ||
85 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | ||
86 | |||
87 | expect(body.username).to.equal('laguna') | ||
88 | expect(body.account.displayName).to.equal('Laguna Loire') | ||
89 | expect(body.role.id).to.equal(UserRole.USER) | ||
90 | |||
91 | lagunaId = body.id | ||
92 | } | ||
93 | }) | ||
94 | |||
95 | it('Should refresh crash token, but not laguna token', async function () { | ||
96 | { | ||
97 | const resRefresh = await server.login.refreshToken({ refreshToken: crashRefreshToken }) | ||
98 | crashAccessToken = resRefresh.body.access_token | ||
99 | crashRefreshToken = resRefresh.body.refresh_token | ||
100 | |||
101 | const body = await server.users.getMyInfo({ token: crashAccessToken }) | ||
102 | expect(body.username).to.equal('crash') | ||
103 | } | ||
104 | |||
105 | { | ||
106 | await server.login.refreshToken({ refreshToken: lagunaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
107 | } | ||
108 | }) | ||
109 | |||
110 | it('Should update Crash profile', async function () { | ||
111 | await server.users.updateMe({ | ||
112 | token: crashAccessToken, | ||
113 | displayName: 'Beautiful Crash', | ||
114 | description: 'Mutant eastern barred bandicoot' | ||
115 | }) | ||
116 | |||
117 | const body = await server.users.getMyInfo({ token: crashAccessToken }) | ||
118 | |||
119 | expect(body.account.displayName).to.equal('Beautiful Crash') | ||
120 | expect(body.account.description).to.equal('Mutant eastern barred bandicoot') | ||
121 | }) | ||
122 | |||
123 | it('Should logout Crash', async function () { | ||
124 | await server.login.logout({ token: crashAccessToken }) | ||
125 | }) | ||
126 | |||
127 | it('Should have logged out Crash', async function () { | ||
128 | await server.servers.waitUntilLog('On logout for auth 1 - 2') | ||
129 | |||
130 | await server.users.getMyInfo({ token: crashAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
131 | }) | ||
132 | |||
133 | it('Should login Crash and keep the old existing profile', async function () { | ||
134 | crashAccessToken = await server.login.getAccessToken({ username: 'crash', password: 'crash password' }) | ||
135 | |||
136 | const body = await server.users.getMyInfo({ token: crashAccessToken }) | ||
137 | |||
138 | expect(body.username).to.equal('crash') | ||
139 | expect(body.account.displayName).to.equal('Beautiful Crash') | ||
140 | expect(body.account.description).to.equal('Mutant eastern barred bandicoot') | ||
141 | expect(body.role.id).to.equal(UserRole.MODERATOR) | ||
142 | }) | ||
143 | |||
144 | it('Should login Laguna and update the profile', async function () { | ||
145 | { | ||
146 | await server.users.update({ userId: lagunaId, videoQuota: 43000, videoQuotaDaily: 43100 }) | ||
147 | await server.users.updateMe({ token: lagunaAccessToken, displayName: 'laguna updated' }) | ||
148 | |||
149 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | ||
150 | expect(body.username).to.equal('laguna') | ||
151 | expect(body.account.displayName).to.equal('laguna updated') | ||
152 | expect(body.videoQuota).to.equal(43000) | ||
153 | expect(body.videoQuotaDaily).to.equal(43100) | ||
154 | } | ||
155 | |||
156 | { | ||
157 | const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } }) | ||
158 | lagunaAccessToken = body.access_token | ||
159 | lagunaRefreshToken = body.refresh_token | ||
160 | } | ||
161 | |||
162 | { | ||
163 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | ||
164 | expect(body.username).to.equal('laguna') | ||
165 | expect(body.account.displayName).to.equal('Laguna Loire') | ||
166 | expect(body.videoQuota).to.equal(42000) | ||
167 | expect(body.videoQuotaDaily).to.equal(43100) | ||
168 | } | ||
169 | }) | ||
170 | |||
171 | it('Should reject token of laguna by the plugin hook', async function () { | ||
172 | await wait(5000) | ||
173 | |||
174 | await server.users.getMyInfo({ token: lagunaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
175 | }) | ||
176 | |||
177 | it('Should reject an invalid username, email, role or display name', async function () { | ||
178 | const command = server.login | ||
179 | |||
180 | await command.login({ user: { username: 'ward', password: 'ward password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
181 | await server.servers.waitUntilLog('valid username') | ||
182 | |||
183 | await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
184 | await server.servers.waitUntilLog('valid displayName') | ||
185 | |||
186 | await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
187 | await server.servers.waitUntilLog('valid role') | ||
188 | |||
189 | await command.login({ user: { username: 'ellone', password: 'elonne password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
190 | await server.servers.waitUntilLog('valid email') | ||
191 | }) | ||
192 | |||
193 | it('Should unregister spyro-auth and do not login existing Spyro', async function () { | ||
194 | await server.plugins.updateSettings({ | ||
195 | npmName: 'peertube-plugin-test-id-pass-auth-one', | ||
196 | settings: { disableSpyro: true } | ||
197 | }) | ||
198 | |||
199 | const command = server.login | ||
200 | await command.login({ user: { username: 'spyro', password: 'spyro password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
201 | await command.login({ user: { username: 'spyro', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
202 | }) | ||
203 | |||
204 | it('Should have disabled this auth', async function () { | ||
205 | const config = await server.config.getConfig() | ||
206 | |||
207 | const auths = config.plugin.registeredIdAndPassAuths | ||
208 | expect(auths).to.have.lengthOf(7) | ||
209 | |||
210 | const spyroAuth = auths.find(a => a.authName === 'spyro-auth') | ||
211 | expect(spyroAuth).to.not.exist | ||
212 | }) | ||
213 | |||
214 | it('Should uninstall the plugin one and do not login existing Crash', async function () { | ||
215 | await server.plugins.uninstall({ npmName: 'peertube-plugin-test-id-pass-auth-one' }) | ||
216 | |||
217 | await server.login.login({ | ||
218 | user: { username: 'crash', password: 'crash password' }, | ||
219 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
220 | }) | ||
221 | }) | ||
222 | |||
223 | it('Should display the correct configuration', async function () { | ||
224 | const config = await server.config.getConfig() | ||
225 | |||
226 | const auths = config.plugin.registeredIdAndPassAuths | ||
227 | expect(auths).to.have.lengthOf(6) | ||
228 | |||
229 | const crashAuth = auths.find(a => a.authName === 'crash-auth') | ||
230 | expect(crashAuth).to.not.exist | ||
231 | }) | ||
232 | |||
233 | it('Should display plugin auth information in users list', async function () { | ||
234 | const { data } = await server.users.list() | ||
235 | |||
236 | const root = data.find(u => u.username === 'root') | ||
237 | const crash = data.find(u => u.username === 'crash') | ||
238 | const laguna = data.find(u => u.username === 'laguna') | ||
239 | |||
240 | expect(root.pluginAuth).to.be.null | ||
241 | expect(crash.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-one') | ||
242 | expect(laguna.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-two') | ||
243 | }) | ||
244 | |||
245 | after(async function () { | ||
246 | await cleanupTests([ server ]) | ||
247 | }) | ||
248 | }) | ||
diff --git a/packages/tests/src/plugins/index.ts b/packages/tests/src/plugins/index.ts new file mode 100644 index 000000000..210af7236 --- /dev/null +++ b/packages/tests/src/plugins/index.ts | |||
@@ -0,0 +1,13 @@ | |||
1 | import './action-hooks' | ||
2 | import './external-auth' | ||
3 | import './filter-hooks' | ||
4 | import './html-injection' | ||
5 | import './id-and-pass-auth' | ||
6 | import './plugin-helpers' | ||
7 | import './plugin-router' | ||
8 | import './plugin-storage' | ||
9 | import './plugin-transcoding' | ||
10 | import './plugin-unloading' | ||
11 | import './plugin-websocket' | ||
12 | import './translations' | ||
13 | import './video-constants' | ||
diff --git a/packages/tests/src/plugins/plugin-helpers.ts b/packages/tests/src/plugins/plugin-helpers.ts new file mode 100644 index 000000000..d2bd8596e --- /dev/null +++ b/packages/tests/src/plugins/plugin-helpers.ts | |||
@@ -0,0 +1,383 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { HttpStatusCode, ThumbnailType } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | makeRawRequest, | ||
13 | PeerTubeServer, | ||
14 | PluginsCommand, | ||
15 | setAccessTokensToServers, | ||
16 | waitJobs | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | import { checkVideoFilesWereRemoved } from '@tests/shared/videos.js' | ||
19 | |||
20 | function postCommand (server: PeerTubeServer, command: string, bodyArg?: object) { | ||
21 | const body = { command } | ||
22 | if (bodyArg) Object.assign(body, bodyArg) | ||
23 | |||
24 | return makePostBodyRequest({ | ||
25 | url: server.url, | ||
26 | path: '/plugins/test-four/router/commander', | ||
27 | fields: body, | ||
28 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
29 | }) | ||
30 | } | ||
31 | |||
32 | describe('Test plugin helpers', function () { | ||
33 | let servers: PeerTubeServer[] | ||
34 | |||
35 | before(async function () { | ||
36 | this.timeout(60000) | ||
37 | |||
38 | servers = await createMultipleServers(2) | ||
39 | await setAccessTokensToServers(servers) | ||
40 | |||
41 | await doubleFollow(servers[0], servers[1]) | ||
42 | |||
43 | await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-four') }) | ||
44 | }) | ||
45 | |||
46 | describe('Logger', function () { | ||
47 | |||
48 | it('Should have logged things', async function () { | ||
49 | await servers[0].servers.waitUntilLog(servers[0].host + ' peertube-plugin-test-four', 1, false) | ||
50 | await servers[0].servers.waitUntilLog('Hello world from plugin four', 1) | ||
51 | }) | ||
52 | }) | ||
53 | |||
54 | describe('Database', function () { | ||
55 | |||
56 | it('Should have made a query', async function () { | ||
57 | await servers[0].servers.waitUntilLog(`root email is admin${servers[0].internalServerNumber}@example.com`) | ||
58 | }) | ||
59 | }) | ||
60 | |||
61 | describe('Config', function () { | ||
62 | |||
63 | it('Should have the correct webserver url', async function () { | ||
64 | await servers[0].servers.waitUntilLog(`server url is ${servers[0].url}`) | ||
65 | }) | ||
66 | |||
67 | it('Should have the correct listening config', async function () { | ||
68 | const res = await makeGetRequest({ | ||
69 | url: servers[0].url, | ||
70 | path: '/plugins/test-four/router/server-listening-config', | ||
71 | expectedStatus: HttpStatusCode.OK_200 | ||
72 | }) | ||
73 | |||
74 | expect(res.body.config).to.exist | ||
75 | expect(res.body.config.hostname).to.equal('::') | ||
76 | expect(res.body.config.port).to.equal(servers[0].port) | ||
77 | }) | ||
78 | |||
79 | it('Should have the correct config', async function () { | ||
80 | const res = await makeGetRequest({ | ||
81 | url: servers[0].url, | ||
82 | path: '/plugins/test-four/router/server-config', | ||
83 | expectedStatus: HttpStatusCode.OK_200 | ||
84 | }) | ||
85 | |||
86 | expect(res.body.serverConfig).to.exist | ||
87 | expect(res.body.serverConfig.instance.name).to.equal('PeerTube') | ||
88 | }) | ||
89 | }) | ||
90 | |||
91 | describe('Server', function () { | ||
92 | |||
93 | it('Should get the server actor', async function () { | ||
94 | await servers[0].servers.waitUntilLog('server actor name is peertube') | ||
95 | }) | ||
96 | }) | ||
97 | |||
98 | describe('Socket', function () { | ||
99 | |||
100 | it('Should sendNotification without any exceptions', async () => { | ||
101 | const user = await servers[0].users.create({ username: 'notis_redding', password: 'secret1234?' }) | ||
102 | await makePostBodyRequest({ | ||
103 | url: servers[0].url, | ||
104 | path: '/plugins/test-four/router/send-notification', | ||
105 | fields: { | ||
106 | userId: user.id | ||
107 | }, | ||
108 | expectedStatus: HttpStatusCode.CREATED_201 | ||
109 | }) | ||
110 | }) | ||
111 | |||
112 | it('Should sendVideoLiveNewState without any exceptions', async () => { | ||
113 | const res = await servers[0].videos.quickUpload({ name: 'video server 1' }) | ||
114 | |||
115 | await makePostBodyRequest({ | ||
116 | url: servers[0].url, | ||
117 | path: '/plugins/test-four/router/send-video-live-new-state/' + res.uuid, | ||
118 | expectedStatus: HttpStatusCode.CREATED_201 | ||
119 | }) | ||
120 | |||
121 | await servers[0].videos.remove({ id: res.uuid }) | ||
122 | }) | ||
123 | }) | ||
124 | |||
125 | describe('Plugin', function () { | ||
126 | |||
127 | it('Should get the base static route', async function () { | ||
128 | const res = await makeGetRequest({ | ||
129 | url: servers[0].url, | ||
130 | path: '/plugins/test-four/router/static-route', | ||
131 | expectedStatus: HttpStatusCode.OK_200 | ||
132 | }) | ||
133 | |||
134 | expect(res.body.staticRoute).to.equal('/plugins/test-four/0.0.1/static/') | ||
135 | }) | ||
136 | |||
137 | it('Should get the base static route', async function () { | ||
138 | const baseRouter = '/plugins/test-four/0.0.1/router/' | ||
139 | |||
140 | const res = await makeGetRequest({ | ||
141 | url: servers[0].url, | ||
142 | path: baseRouter + 'router-route', | ||
143 | expectedStatus: HttpStatusCode.OK_200 | ||
144 | }) | ||
145 | |||
146 | expect(res.body.routerRoute).to.equal(baseRouter) | ||
147 | }) | ||
148 | }) | ||
149 | |||
150 | describe('User', function () { | ||
151 | let rootId: number | ||
152 | |||
153 | it('Should not get a user if not authenticated', async function () { | ||
154 | await makeGetRequest({ | ||
155 | url: servers[0].url, | ||
156 | path: '/plugins/test-four/router/user', | ||
157 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
158 | }) | ||
159 | }) | ||
160 | |||
161 | it('Should get a user if authenticated', async function () { | ||
162 | const res = await makeGetRequest({ | ||
163 | url: servers[0].url, | ||
164 | token: servers[0].accessToken, | ||
165 | path: '/plugins/test-four/router/user', | ||
166 | expectedStatus: HttpStatusCode.OK_200 | ||
167 | }) | ||
168 | |||
169 | expect(res.body.username).to.equal('root') | ||
170 | expect(res.body.displayName).to.equal('root') | ||
171 | expect(res.body.isAdmin).to.be.true | ||
172 | expect(res.body.isModerator).to.be.false | ||
173 | expect(res.body.isUser).to.be.false | ||
174 | |||
175 | rootId = res.body.id | ||
176 | }) | ||
177 | |||
178 | it('Should load a user by id', async function () { | ||
179 | { | ||
180 | const res = await makeGetRequest({ | ||
181 | url: servers[0].url, | ||
182 | path: '/plugins/test-four/router/user/' + rootId, | ||
183 | expectedStatus: HttpStatusCode.OK_200 | ||
184 | }) | ||
185 | |||
186 | expect(res.body.username).to.equal('root') | ||
187 | } | ||
188 | |||
189 | { | ||
190 | await makeGetRequest({ | ||
191 | url: servers[0].url, | ||
192 | path: '/plugins/test-four/router/user/42', | ||
193 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
194 | }) | ||
195 | } | ||
196 | }) | ||
197 | }) | ||
198 | |||
199 | describe('Moderation', function () { | ||
200 | let videoUUIDServer1: string | ||
201 | |||
202 | before(async function () { | ||
203 | this.timeout(60000) | ||
204 | |||
205 | { | ||
206 | const res = await servers[0].videos.quickUpload({ name: 'video server 1' }) | ||
207 | videoUUIDServer1 = res.uuid | ||
208 | } | ||
209 | |||
210 | { | ||
211 | await servers[1].videos.quickUpload({ name: 'video server 2' }) | ||
212 | } | ||
213 | |||
214 | await waitJobs(servers) | ||
215 | |||
216 | const { data } = await servers[0].videos.list() | ||
217 | |||
218 | expect(data).to.have.lengthOf(2) | ||
219 | }) | ||
220 | |||
221 | it('Should mute server 2', async function () { | ||
222 | await postCommand(servers[0], 'blockServer', { hostToBlock: servers[1].host }) | ||
223 | |||
224 | const { data } = await servers[0].videos.list() | ||
225 | |||
226 | expect(data).to.have.lengthOf(1) | ||
227 | expect(data[0].name).to.equal('video server 1') | ||
228 | }) | ||
229 | |||
230 | it('Should unmute server 2', async function () { | ||
231 | await postCommand(servers[0], 'unblockServer', { hostToUnblock: servers[1].host }) | ||
232 | |||
233 | const { data } = await servers[0].videos.list() | ||
234 | |||
235 | expect(data).to.have.lengthOf(2) | ||
236 | }) | ||
237 | |||
238 | it('Should mute account of server 2', async function () { | ||
239 | await postCommand(servers[0], 'blockAccount', { handleToBlock: `root@${servers[1].host}` }) | ||
240 | |||
241 | const { data } = await servers[0].videos.list() | ||
242 | |||
243 | expect(data).to.have.lengthOf(1) | ||
244 | expect(data[0].name).to.equal('video server 1') | ||
245 | }) | ||
246 | |||
247 | it('Should unmute account of server 2', async function () { | ||
248 | await postCommand(servers[0], 'unblockAccount', { handleToUnblock: `root@${servers[1].host}` }) | ||
249 | |||
250 | const { data } = await servers[0].videos.list() | ||
251 | |||
252 | expect(data).to.have.lengthOf(2) | ||
253 | }) | ||
254 | |||
255 | it('Should blacklist video', async function () { | ||
256 | await postCommand(servers[0], 'blacklist', { videoUUID: videoUUIDServer1, unfederate: true }) | ||
257 | |||
258 | await waitJobs(servers) | ||
259 | |||
260 | for (const server of servers) { | ||
261 | const { data } = await server.videos.list() | ||
262 | |||
263 | expect(data).to.have.lengthOf(1) | ||
264 | expect(data[0].name).to.equal('video server 2') | ||
265 | } | ||
266 | }) | ||
267 | |||
268 | it('Should unblacklist video', async function () { | ||
269 | await postCommand(servers[0], 'unblacklist', { videoUUID: videoUUIDServer1 }) | ||
270 | |||
271 | await waitJobs(servers) | ||
272 | |||
273 | for (const server of servers) { | ||
274 | const { data } = await server.videos.list() | ||
275 | |||
276 | expect(data).to.have.lengthOf(2) | ||
277 | } | ||
278 | }) | ||
279 | }) | ||
280 | |||
281 | describe('Videos', function () { | ||
282 | let videoUUID: string | ||
283 | let videoPath: string | ||
284 | |||
285 | before(async function () { | ||
286 | this.timeout(240000) | ||
287 | |||
288 | await servers[0].config.enableTranscoding() | ||
289 | |||
290 | const res = await servers[0].videos.quickUpload({ name: 'video1' }) | ||
291 | videoUUID = res.uuid | ||
292 | |||
293 | await waitJobs(servers) | ||
294 | }) | ||
295 | |||
296 | it('Should get video files', async function () { | ||
297 | const { body } = await makeGetRequest({ | ||
298 | url: servers[0].url, | ||
299 | path: '/plugins/test-four/router/video-files/' + videoUUID, | ||
300 | expectedStatus: HttpStatusCode.OK_200 | ||
301 | }) | ||
302 | |||
303 | // Video files check | ||
304 | { | ||
305 | expect(body.webVideo.videoFiles).to.be.an('array') | ||
306 | expect(body.hls.videoFiles).to.be.an('array') | ||
307 | |||
308 | for (const resolution of [ 144, 240, 360, 480, 720 ]) { | ||
309 | for (const files of [ body.webVideo.videoFiles, body.hls.videoFiles ]) { | ||
310 | const file = files.find(f => f.resolution === resolution) | ||
311 | expect(file).to.exist | ||
312 | |||
313 | expect(file.size).to.be.a('number') | ||
314 | expect(file.fps).to.equal(25) | ||
315 | |||
316 | expect(await pathExists(file.path)).to.be.true | ||
317 | await makeRawRequest({ url: file.url, expectedStatus: HttpStatusCode.OK_200 }) | ||
318 | } | ||
319 | } | ||
320 | |||
321 | videoPath = body.webVideo.videoFiles[0].path | ||
322 | } | ||
323 | |||
324 | // Thumbnails check | ||
325 | { | ||
326 | expect(body.thumbnails).to.be.an('array') | ||
327 | |||
328 | const miniature = body.thumbnails.find(t => t.type === ThumbnailType.MINIATURE) | ||
329 | expect(miniature).to.exist | ||
330 | expect(await pathExists(miniature.path)).to.be.true | ||
331 | await makeRawRequest({ url: miniature.url, expectedStatus: HttpStatusCode.OK_200 }) | ||
332 | |||
333 | const preview = body.thumbnails.find(t => t.type === ThumbnailType.PREVIEW) | ||
334 | expect(preview).to.exist | ||
335 | expect(await pathExists(preview.path)).to.be.true | ||
336 | await makeRawRequest({ url: preview.url, expectedStatus: HttpStatusCode.OK_200 }) | ||
337 | } | ||
338 | }) | ||
339 | |||
340 | it('Should probe a file', async function () { | ||
341 | const { body } = await makeGetRequest({ | ||
342 | url: servers[0].url, | ||
343 | path: '/plugins/test-four/router/ffprobe', | ||
344 | query: { | ||
345 | path: videoPath | ||
346 | }, | ||
347 | expectedStatus: HttpStatusCode.OK_200 | ||
348 | }) | ||
349 | |||
350 | expect(body.streams).to.be.an('array') | ||
351 | expect(body.streams).to.have.lengthOf(2) | ||
352 | }) | ||
353 | |||
354 | it('Should remove a video after a view', async function () { | ||
355 | this.timeout(40000) | ||
356 | |||
357 | // Should not throw -> video exists | ||
358 | const video = await servers[0].videos.get({ id: videoUUID }) | ||
359 | // Should delete the video | ||
360 | await servers[0].views.simulateView({ id: videoUUID }) | ||
361 | |||
362 | await servers[0].servers.waitUntilLog('Video deleted by plugin four.') | ||
363 | |||
364 | try { | ||
365 | // Should throw because the video should have been deleted | ||
366 | await servers[0].videos.get({ id: videoUUID }) | ||
367 | throw new Error('Video exists') | ||
368 | } catch (err) { | ||
369 | if (err.message.includes('exists')) throw err | ||
370 | } | ||
371 | |||
372 | await checkVideoFilesWereRemoved({ server: servers[0], video }) | ||
373 | }) | ||
374 | |||
375 | it('Should have fetched the video by URL', async function () { | ||
376 | await servers[0].servers.waitUntilLog(`video from DB uuid is ${videoUUID}`) | ||
377 | }) | ||
378 | }) | ||
379 | |||
380 | after(async function () { | ||
381 | await cleanupTests(servers) | ||
382 | }) | ||
383 | }) | ||
diff --git a/packages/tests/src/plugins/plugin-router.ts b/packages/tests/src/plugins/plugin-router.ts new file mode 100644 index 000000000..6f3571c05 --- /dev/null +++ b/packages/tests/src/plugins/plugin-router.ts | |||
@@ -0,0 +1,105 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makeGetRequest, | ||
8 | makePostBodyRequest, | ||
9 | PeerTubeServer, | ||
10 | PluginsCommand, | ||
11 | setAccessTokensToServers | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
14 | |||
15 | describe('Test plugin helpers', function () { | ||
16 | let server: PeerTubeServer | ||
17 | const basePaths = [ | ||
18 | '/plugins/test-five/router/', | ||
19 | '/plugins/test-five/0.0.1/router/' | ||
20 | ] | ||
21 | |||
22 | before(async function () { | ||
23 | this.timeout(30000) | ||
24 | |||
25 | server = await createSingleServer(1) | ||
26 | await setAccessTokensToServers([ server ]) | ||
27 | |||
28 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-five') }) | ||
29 | }) | ||
30 | |||
31 | it('Should answer "pong"', async function () { | ||
32 | for (const path of basePaths) { | ||
33 | const res = await makeGetRequest({ | ||
34 | url: server.url, | ||
35 | path: path + 'ping', | ||
36 | expectedStatus: HttpStatusCode.OK_200 | ||
37 | }) | ||
38 | |||
39 | expect(res.body.message).to.equal('pong') | ||
40 | } | ||
41 | }) | ||
42 | |||
43 | it('Should check if authenticated', async function () { | ||
44 | for (const path of basePaths) { | ||
45 | const res = await makeGetRequest({ | ||
46 | url: server.url, | ||
47 | path: path + 'is-authenticated', | ||
48 | token: server.accessToken, | ||
49 | expectedStatus: 200 | ||
50 | }) | ||
51 | |||
52 | expect(res.body.isAuthenticated).to.equal(true) | ||
53 | |||
54 | const secRes = await makeGetRequest({ | ||
55 | url: server.url, | ||
56 | path: path + 'is-authenticated', | ||
57 | expectedStatus: 200 | ||
58 | }) | ||
59 | |||
60 | expect(secRes.body.isAuthenticated).to.equal(false) | ||
61 | } | ||
62 | }) | ||
63 | |||
64 | it('Should mirror post body', async function () { | ||
65 | const body = { | ||
66 | hello: 'world', | ||
67 | riri: 'fifi', | ||
68 | loulou: 'picsou' | ||
69 | } | ||
70 | |||
71 | for (const path of basePaths) { | ||
72 | const res = await makePostBodyRequest({ | ||
73 | url: server.url, | ||
74 | path: path + 'form/post/mirror', | ||
75 | fields: body, | ||
76 | expectedStatus: HttpStatusCode.OK_200 | ||
77 | }) | ||
78 | |||
79 | expect(res.body).to.deep.equal(body) | ||
80 | } | ||
81 | }) | ||
82 | |||
83 | it('Should remove the plugin and remove the routes', async function () { | ||
84 | await server.plugins.uninstall({ npmName: 'peertube-plugin-test-five' }) | ||
85 | |||
86 | for (const path of basePaths) { | ||
87 | await makeGetRequest({ | ||
88 | url: server.url, | ||
89 | path: path + 'ping', | ||
90 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
91 | }) | ||
92 | |||
93 | await makePostBodyRequest({ | ||
94 | url: server.url, | ||
95 | path: path + 'ping', | ||
96 | fields: {}, | ||
97 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
98 | }) | ||
99 | } | ||
100 | }) | ||
101 | |||
102 | after(async function () { | ||
103 | await cleanupTests([ server ]) | ||
104 | }) | ||
105 | }) | ||
diff --git a/packages/tests/src/plugins/plugin-storage.ts b/packages/tests/src/plugins/plugin-storage.ts new file mode 100644 index 000000000..f9b0ead0c --- /dev/null +++ b/packages/tests/src/plugins/plugin-storage.ts | |||
@@ -0,0 +1,95 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { readdir, readFile } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | makeGetRequest, | ||
12 | PeerTubeServer, | ||
13 | PluginsCommand, | ||
14 | setAccessTokensToServers | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('Test plugin storage', function () { | ||
18 | let server: PeerTubeServer | ||
19 | |||
20 | before(async function () { | ||
21 | this.timeout(30000) | ||
22 | |||
23 | server = await createSingleServer(1) | ||
24 | await setAccessTokensToServers([ server ]) | ||
25 | |||
26 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') }) | ||
27 | }) | ||
28 | |||
29 | describe('DB storage', function () { | ||
30 | it('Should correctly store a subkey', async function () { | ||
31 | await server.servers.waitUntilLog('superkey stored value is toto') | ||
32 | }) | ||
33 | |||
34 | it('Should correctly retrieve an array as array from the storage.', async function () { | ||
35 | await server.servers.waitUntilLog('storedArrayKey isArray is true') | ||
36 | await server.servers.waitUntilLog('storedArrayKey stored value is toto, toto2') | ||
37 | }) | ||
38 | }) | ||
39 | |||
40 | describe('Disk storage', function () { | ||
41 | let dataPath: string | ||
42 | let pluginDataPath: string | ||
43 | |||
44 | async function getFileContent () { | ||
45 | const files = await readdir(pluginDataPath) | ||
46 | expect(files).to.have.lengthOf(1) | ||
47 | |||
48 | return readFile(join(pluginDataPath, files[0]), 'utf8') | ||
49 | } | ||
50 | |||
51 | before(function () { | ||
52 | dataPath = server.servers.buildDirectory('plugins/data') | ||
53 | pluginDataPath = join(dataPath, 'peertube-plugin-test-six') | ||
54 | }) | ||
55 | |||
56 | it('Should have created the directory on install', async function () { | ||
57 | const dataPath = server.servers.buildDirectory('plugins/data') | ||
58 | const pluginDataPath = join(dataPath, 'peertube-plugin-test-six') | ||
59 | |||
60 | expect(await pathExists(dataPath)).to.be.true | ||
61 | expect(await pathExists(pluginDataPath)).to.be.true | ||
62 | expect(await readdir(pluginDataPath)).to.have.lengthOf(0) | ||
63 | }) | ||
64 | |||
65 | it('Should have created a file', async function () { | ||
66 | await makeGetRequest({ | ||
67 | url: server.url, | ||
68 | token: server.accessToken, | ||
69 | path: '/plugins/test-six/router/create-file', | ||
70 | expectedStatus: HttpStatusCode.OK_200 | ||
71 | }) | ||
72 | |||
73 | const content = await getFileContent() | ||
74 | expect(content).to.equal('Prince Ali') | ||
75 | }) | ||
76 | |||
77 | it('Should still have the file after an uninstallation', async function () { | ||
78 | await server.plugins.uninstall({ npmName: 'peertube-plugin-test-six' }) | ||
79 | |||
80 | const content = await getFileContent() | ||
81 | expect(content).to.equal('Prince Ali') | ||
82 | }) | ||
83 | |||
84 | it('Should still have the file after the reinstallation', async function () { | ||
85 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') }) | ||
86 | |||
87 | const content = await getFileContent() | ||
88 | expect(content).to.equal('Prince Ali') | ||
89 | }) | ||
90 | }) | ||
91 | |||
92 | after(async function () { | ||
93 | await cleanupTests([ server ]) | ||
94 | }) | ||
95 | }) | ||
diff --git a/packages/tests/src/plugins/plugin-transcoding.ts b/packages/tests/src/plugins/plugin-transcoding.ts new file mode 100644 index 000000000..2f50f65ff --- /dev/null +++ b/packages/tests/src/plugins/plugin-transcoding.ts | |||
@@ -0,0 +1,279 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { getAudioStream, getVideoStream, getVideoStreamFPS } from '@peertube/peertube-ffmpeg' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | PluginsCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | testFfmpegStreamError, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | async function createLiveWrapper (server: PeerTubeServer) { | ||
18 | const liveAttributes = { | ||
19 | name: 'live video', | ||
20 | channelId: server.store.channel.id, | ||
21 | privacy: VideoPrivacy.PUBLIC | ||
22 | } | ||
23 | |||
24 | const { uuid } = await server.live.create({ fields: liveAttributes }) | ||
25 | |||
26 | return uuid | ||
27 | } | ||
28 | |||
29 | function updateConf (server: PeerTubeServer, vodProfile: string, liveProfile: string) { | ||
30 | return server.config.updateCustomSubConfig({ | ||
31 | newConfig: { | ||
32 | transcoding: { | ||
33 | enabled: true, | ||
34 | profile: vodProfile, | ||
35 | hls: { | ||
36 | enabled: true | ||
37 | }, | ||
38 | webVideos: { | ||
39 | enabled: true | ||
40 | }, | ||
41 | resolutions: { | ||
42 | '240p': true, | ||
43 | '360p': false, | ||
44 | '480p': false, | ||
45 | '720p': true | ||
46 | } | ||
47 | }, | ||
48 | live: { | ||
49 | transcoding: { | ||
50 | profile: liveProfile, | ||
51 | enabled: true, | ||
52 | resolutions: { | ||
53 | '240p': true, | ||
54 | '360p': false, | ||
55 | '480p': false, | ||
56 | '720p': true | ||
57 | } | ||
58 | } | ||
59 | } | ||
60 | } | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | describe('Test transcoding plugins', function () { | ||
65 | let server: PeerTubeServer | ||
66 | |||
67 | before(async function () { | ||
68 | this.timeout(60000) | ||
69 | |||
70 | server = await createSingleServer(1) | ||
71 | await setAccessTokensToServers([ server ]) | ||
72 | await setDefaultVideoChannel([ server ]) | ||
73 | |||
74 | await updateConf(server, 'default', 'default') | ||
75 | }) | ||
76 | |||
77 | describe('When using a plugin adding profiles to existing encoders', function () { | ||
78 | |||
79 | async function checkVideoFPS (uuid: string, type: 'above' | 'below', fps: number) { | ||
80 | const video = await server.videos.get({ id: uuid }) | ||
81 | const files = video.files.concat(...video.streamingPlaylists.map(p => p.files)) | ||
82 | |||
83 | for (const file of files) { | ||
84 | if (type === 'above') { | ||
85 | expect(file.fps).to.be.above(fps) | ||
86 | } else { | ||
87 | expect(file.fps).to.be.below(fps) | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | |||
92 | async function checkLiveFPS (uuid: string, type: 'above' | 'below', fps: number) { | ||
93 | const playlistUrl = `${server.url}/static/streaming-playlists/hls/${uuid}/0.m3u8` | ||
94 | const videoFPS = await getVideoStreamFPS(playlistUrl) | ||
95 | |||
96 | if (type === 'above') { | ||
97 | expect(videoFPS).to.be.above(fps) | ||
98 | } else { | ||
99 | expect(videoFPS).to.be.below(fps) | ||
100 | } | ||
101 | } | ||
102 | |||
103 | before(async function () { | ||
104 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-one') }) | ||
105 | }) | ||
106 | |||
107 | it('Should have the appropriate available profiles', async function () { | ||
108 | const config = await server.config.getConfig() | ||
109 | |||
110 | expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'input-options-vod', 'bad-scale-vod' ]) | ||
111 | expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'high-live', 'input-options-live', 'bad-scale-live' ]) | ||
112 | }) | ||
113 | |||
114 | describe('VOD', function () { | ||
115 | |||
116 | it('Should not use the plugin profile if not chosen by the admin', async function () { | ||
117 | this.timeout(240000) | ||
118 | |||
119 | const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid | ||
120 | await waitJobs([ server ]) | ||
121 | |||
122 | await checkVideoFPS(videoUUID, 'above', 20) | ||
123 | }) | ||
124 | |||
125 | it('Should use the vod profile', async function () { | ||
126 | this.timeout(240000) | ||
127 | |||
128 | await updateConf(server, 'low-vod', 'default') | ||
129 | |||
130 | const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid | ||
131 | await waitJobs([ server ]) | ||
132 | |||
133 | await checkVideoFPS(videoUUID, 'below', 12) | ||
134 | }) | ||
135 | |||
136 | it('Should apply input options in vod profile', async function () { | ||
137 | this.timeout(240000) | ||
138 | |||
139 | await updateConf(server, 'input-options-vod', 'default') | ||
140 | |||
141 | const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid | ||
142 | await waitJobs([ server ]) | ||
143 | |||
144 | await checkVideoFPS(videoUUID, 'below', 6) | ||
145 | }) | ||
146 | |||
147 | it('Should apply the scale filter in vod profile', async function () { | ||
148 | this.timeout(240000) | ||
149 | |||
150 | await updateConf(server, 'bad-scale-vod', 'default') | ||
151 | |||
152 | const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid | ||
153 | await waitJobs([ server ]) | ||
154 | |||
155 | // Transcoding failed | ||
156 | const video = await server.videos.get({ id: videoUUID }) | ||
157 | expect(video.files).to.have.lengthOf(1) | ||
158 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
159 | }) | ||
160 | }) | ||
161 | |||
162 | describe('Live', function () { | ||
163 | |||
164 | it('Should not use the plugin profile if not chosen by the admin', async function () { | ||
165 | this.timeout(240000) | ||
166 | |||
167 | const liveVideoId = await createLiveWrapper(server) | ||
168 | |||
169 | await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' }) | ||
170 | await server.live.waitUntilPublished({ videoId: liveVideoId }) | ||
171 | await waitJobs([ server ]) | ||
172 | |||
173 | await checkLiveFPS(liveVideoId, 'above', 20) | ||
174 | }) | ||
175 | |||
176 | it('Should use the live profile', async function () { | ||
177 | this.timeout(240000) | ||
178 | |||
179 | await updateConf(server, 'low-vod', 'high-live') | ||
180 | |||
181 | const liveVideoId = await createLiveWrapper(server) | ||
182 | |||
183 | await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' }) | ||
184 | await server.live.waitUntilPublished({ videoId: liveVideoId }) | ||
185 | await waitJobs([ server ]) | ||
186 | |||
187 | await checkLiveFPS(liveVideoId, 'above', 45) | ||
188 | }) | ||
189 | |||
190 | it('Should apply the input options on live profile', async function () { | ||
191 | this.timeout(240000) | ||
192 | |||
193 | await updateConf(server, 'low-vod', 'input-options-live') | ||
194 | |||
195 | const liveVideoId = await createLiveWrapper(server) | ||
196 | |||
197 | await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' }) | ||
198 | await server.live.waitUntilPublished({ videoId: liveVideoId }) | ||
199 | await waitJobs([ server ]) | ||
200 | |||
201 | await checkLiveFPS(liveVideoId, 'above', 45) | ||
202 | }) | ||
203 | |||
204 | it('Should apply the scale filter name on live profile', async function () { | ||
205 | this.timeout(240000) | ||
206 | |||
207 | await updateConf(server, 'low-vod', 'bad-scale-live') | ||
208 | |||
209 | const liveVideoId = await createLiveWrapper(server) | ||
210 | |||
211 | const command = await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' }) | ||
212 | await testFfmpegStreamError(command, true) | ||
213 | }) | ||
214 | |||
215 | it('Should default to the default profile if the specified profile does not exist', async function () { | ||
216 | this.timeout(240000) | ||
217 | |||
218 | await server.plugins.uninstall({ npmName: 'peertube-plugin-test-transcoding-one' }) | ||
219 | |||
220 | const config = await server.config.getConfig() | ||
221 | |||
222 | expect(config.transcoding.availableProfiles).to.deep.equal([ 'default' ]) | ||
223 | expect(config.live.transcoding.availableProfiles).to.deep.equal([ 'default' ]) | ||
224 | |||
225 | const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid | ||
226 | await waitJobs([ server ]) | ||
227 | |||
228 | await checkVideoFPS(videoUUID, 'above', 20) | ||
229 | }) | ||
230 | }) | ||
231 | |||
232 | }) | ||
233 | |||
234 | describe('When using a plugin adding new encoders', function () { | ||
235 | |||
236 | before(async function () { | ||
237 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-two') }) | ||
238 | |||
239 | await updateConf(server, 'test-vod-profile', 'test-live-profile') | ||
240 | }) | ||
241 | |||
242 | it('Should use the new vod encoders', async function () { | ||
243 | this.timeout(240000) | ||
244 | |||
245 | const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid | ||
246 | await waitJobs([ server ]) | ||
247 | |||
248 | const video = await server.videos.get({ id: videoUUID }) | ||
249 | |||
250 | const path = server.servers.buildWebVideoFilePath(video.files[0].fileUrl) | ||
251 | const audioProbe = await getAudioStream(path) | ||
252 | expect(audioProbe.audioStream.codec_name).to.equal('opus') | ||
253 | |||
254 | const videoProbe = await getVideoStream(path) | ||
255 | expect(videoProbe.codec_name).to.equal('vp9') | ||
256 | }) | ||
257 | |||
258 | it('Should use the new live encoders', async function () { | ||
259 | this.timeout(240000) | ||
260 | |||
261 | const liveVideoId = await createLiveWrapper(server) | ||
262 | |||
263 | await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) | ||
264 | await server.live.waitUntilPublished({ videoId: liveVideoId }) | ||
265 | await waitJobs([ server ]) | ||
266 | |||
267 | const playlistUrl = `${server.url}/static/streaming-playlists/hls/${liveVideoId}/0.m3u8` | ||
268 | const audioProbe = await getAudioStream(playlistUrl) | ||
269 | expect(audioProbe.audioStream.codec_name).to.equal('opus') | ||
270 | |||
271 | const videoProbe = await getVideoStream(playlistUrl) | ||
272 | expect(videoProbe.codec_name).to.equal('h264') | ||
273 | }) | ||
274 | }) | ||
275 | |||
276 | after(async function () { | ||
277 | await cleanupTests([ server ]) | ||
278 | }) | ||
279 | }) | ||
diff --git a/packages/tests/src/plugins/plugin-unloading.ts b/packages/tests/src/plugins/plugin-unloading.ts new file mode 100644 index 000000000..70310bc8c --- /dev/null +++ b/packages/tests/src/plugins/plugin-unloading.ts | |||
@@ -0,0 +1,75 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makeGetRequest, | ||
8 | PeerTubeServer, | ||
9 | PluginsCommand, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
13 | |||
14 | describe('Test plugins module unloading', function () { | ||
15 | let server: PeerTubeServer = null | ||
16 | const requestPath = '/plugins/test-unloading/router/get' | ||
17 | let value: string = null | ||
18 | |||
19 | before(async function () { | ||
20 | this.timeout(30000) | ||
21 | |||
22 | server = await createSingleServer(1) | ||
23 | await setAccessTokensToServers([ server ]) | ||
24 | |||
25 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') }) | ||
26 | }) | ||
27 | |||
28 | it('Should return a numeric value', async function () { | ||
29 | const res = await makeGetRequest({ | ||
30 | url: server.url, | ||
31 | path: requestPath, | ||
32 | expectedStatus: HttpStatusCode.OK_200 | ||
33 | }) | ||
34 | |||
35 | expect(res.body.message).to.match(/^\d+$/) | ||
36 | value = res.body.message | ||
37 | }) | ||
38 | |||
39 | it('Should return the same value the second time', async function () { | ||
40 | const res = await makeGetRequest({ | ||
41 | url: server.url, | ||
42 | path: requestPath, | ||
43 | expectedStatus: HttpStatusCode.OK_200 | ||
44 | }) | ||
45 | |||
46 | expect(res.body.message).to.be.equal(value) | ||
47 | }) | ||
48 | |||
49 | it('Should uninstall the plugin and free the route', async function () { | ||
50 | await server.plugins.uninstall({ npmName: 'peertube-plugin-test-unloading' }) | ||
51 | |||
52 | await makeGetRequest({ | ||
53 | url: server.url, | ||
54 | path: requestPath, | ||
55 | expectedStatus: HttpStatusCode.NOT_FOUND_404 | ||
56 | }) | ||
57 | }) | ||
58 | |||
59 | it('Should return a different numeric value', async function () { | ||
60 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') }) | ||
61 | |||
62 | const res = await makeGetRequest({ | ||
63 | url: server.url, | ||
64 | path: requestPath, | ||
65 | expectedStatus: HttpStatusCode.OK_200 | ||
66 | }) | ||
67 | |||
68 | expect(res.body.message).to.match(/^\d+$/) | ||
69 | expect(res.body.message).to.be.not.equal(value) | ||
70 | }) | ||
71 | |||
72 | after(async function () { | ||
73 | await cleanupTests([ server ]) | ||
74 | }) | ||
75 | }) | ||
diff --git a/packages/tests/src/plugins/plugin-websocket.ts b/packages/tests/src/plugins/plugin-websocket.ts new file mode 100644 index 000000000..832dcebd0 --- /dev/null +++ b/packages/tests/src/plugins/plugin-websocket.ts | |||
@@ -0,0 +1,76 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import WebSocket from 'ws' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | PeerTubeServer, | ||
8 | PluginsCommand, | ||
9 | setAccessTokensToServers | ||
10 | } from '@peertube/peertube-server-commands' | ||
11 | |||
12 | function buildWebSocket (server: PeerTubeServer, path: string) { | ||
13 | return new WebSocket('ws://' + server.host + path) | ||
14 | } | ||
15 | |||
16 | function expectErrorOrTimeout (server: PeerTubeServer, path: string, expectedTimeout: number) { | ||
17 | return new Promise<void>((res, rej) => { | ||
18 | const ws = buildWebSocket(server, path) | ||
19 | ws.on('error', () => res()) | ||
20 | |||
21 | const timeout = setTimeout(() => res(), expectedTimeout) | ||
22 | |||
23 | ws.on('open', () => { | ||
24 | clearTimeout(timeout) | ||
25 | |||
26 | return rej(new Error('Connect did not timeout')) | ||
27 | }) | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | describe('Test plugin websocket', function () { | ||
32 | let server: PeerTubeServer | ||
33 | const basePaths = [ | ||
34 | '/plugins/test-websocket/ws/', | ||
35 | '/plugins/test-websocket/0.0.1/ws/' | ||
36 | ] | ||
37 | |||
38 | before(async function () { | ||
39 | this.timeout(30000) | ||
40 | |||
41 | server = await createSingleServer(1) | ||
42 | await setAccessTokensToServers([ server ]) | ||
43 | |||
44 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-websocket') }) | ||
45 | }) | ||
46 | |||
47 | it('Should not connect to the websocket without the appropriate path', async function () { | ||
48 | const paths = [ | ||
49 | '/plugins/unknown/ws/', | ||
50 | '/plugins/unknown/0.0.1/ws/' | ||
51 | ] | ||
52 | |||
53 | for (const path of paths) { | ||
54 | await expectErrorOrTimeout(server, path, 1000) | ||
55 | } | ||
56 | }) | ||
57 | |||
58 | it('Should not connect to the websocket without the appropriate sub path', async function () { | ||
59 | for (const path of basePaths) { | ||
60 | await expectErrorOrTimeout(server, path + '/unknown', 1000) | ||
61 | } | ||
62 | }) | ||
63 | |||
64 | it('Should connect to the websocket and receive pong', function (done) { | ||
65 | const ws = buildWebSocket(server, basePaths[0]) | ||
66 | |||
67 | ws.on('open', () => ws.send('ping')) | ||
68 | ws.on('message', data => { | ||
69 | if (data.toString() === 'pong') return done() | ||
70 | }) | ||
71 | }) | ||
72 | |||
73 | after(async function () { | ||
74 | await cleanupTests([ server ]) | ||
75 | }) | ||
76 | }) | ||
diff --git a/packages/tests/src/plugins/translations.ts b/packages/tests/src/plugins/translations.ts new file mode 100644 index 000000000..a69e14134 --- /dev/null +++ b/packages/tests/src/plugins/translations.ts | |||
@@ -0,0 +1,80 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | PeerTubeServer, | ||
8 | PluginsCommand, | ||
9 | setAccessTokensToServers | ||
10 | } from '@peertube/peertube-server-commands' | ||
11 | |||
12 | describe('Test plugin translations', function () { | ||
13 | let server: PeerTubeServer | ||
14 | let command: PluginsCommand | ||
15 | |||
16 | before(async function () { | ||
17 | this.timeout(30000) | ||
18 | |||
19 | server = await createSingleServer(1) | ||
20 | await setAccessTokensToServers([ server ]) | ||
21 | |||
22 | command = server.plugins | ||
23 | |||
24 | await command.install({ path: PluginsCommand.getPluginTestPath() }) | ||
25 | await command.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') }) | ||
26 | }) | ||
27 | |||
28 | it('Should not have translations for locale pt', async function () { | ||
29 | const body = await command.getTranslations({ locale: 'pt' }) | ||
30 | |||
31 | expect(body).to.deep.equal({}) | ||
32 | }) | ||
33 | |||
34 | it('Should have translations for locale fr', async function () { | ||
35 | const body = await command.getTranslations({ locale: 'fr-FR' }) | ||
36 | |||
37 | expect(body).to.deep.equal({ | ||
38 | 'peertube-plugin-test': { | ||
39 | Hi: 'Coucou' | ||
40 | }, | ||
41 | 'peertube-plugin-test-filter-translations': { | ||
42 | 'Hello world': 'Bonjour le monde' | ||
43 | } | ||
44 | }) | ||
45 | }) | ||
46 | |||
47 | it('Should have translations of locale it', async function () { | ||
48 | const body = await command.getTranslations({ locale: 'it-IT' }) | ||
49 | |||
50 | expect(body).to.deep.equal({ | ||
51 | 'peertube-plugin-test-filter-translations': { | ||
52 | 'Hello world': 'Ciao, mondo!' | ||
53 | } | ||
54 | }) | ||
55 | }) | ||
56 | |||
57 | it('Should remove the plugin and remove the locales', async function () { | ||
58 | await command.uninstall({ npmName: 'peertube-plugin-test-filter-translations' }) | ||
59 | |||
60 | { | ||
61 | const body = await command.getTranslations({ locale: 'fr-FR' }) | ||
62 | |||
63 | expect(body).to.deep.equal({ | ||
64 | 'peertube-plugin-test': { | ||
65 | Hi: 'Coucou' | ||
66 | } | ||
67 | }) | ||
68 | } | ||
69 | |||
70 | { | ||
71 | const body = await command.getTranslations({ locale: 'it-IT' }) | ||
72 | |||
73 | expect(body).to.deep.equal({}) | ||
74 | } | ||
75 | }) | ||
76 | |||
77 | after(async function () { | ||
78 | await cleanupTests([ server ]) | ||
79 | }) | ||
80 | }) | ||
diff --git a/packages/tests/src/plugins/video-constants.ts b/packages/tests/src/plugins/video-constants.ts new file mode 100644 index 000000000..b81240a64 --- /dev/null +++ b/packages/tests/src/plugins/video-constants.ts | |||
@@ -0,0 +1,180 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makeGetRequest, | ||
8 | PeerTubeServer, | ||
9 | PluginsCommand, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | import { HttpStatusCode, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models' | ||
13 | |||
14 | describe('Test plugin altering video constants', function () { | ||
15 | let server: PeerTubeServer | ||
16 | |||
17 | before(async function () { | ||
18 | this.timeout(30000) | ||
19 | |||
20 | server = await createSingleServer(1) | ||
21 | await setAccessTokensToServers([ server ]) | ||
22 | |||
23 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') }) | ||
24 | }) | ||
25 | |||
26 | it('Should have updated languages', async function () { | ||
27 | const languages = await server.videos.getLanguages() | ||
28 | |||
29 | expect(languages['en']).to.not.exist | ||
30 | expect(languages['fr']).to.not.exist | ||
31 | |||
32 | expect(languages['al_bhed']).to.equal('Al Bhed') | ||
33 | expect(languages['al_bhed2']).to.equal('Al Bhed 2') | ||
34 | expect(languages['al_bhed3']).to.not.exist | ||
35 | }) | ||
36 | |||
37 | it('Should have updated categories', async function () { | ||
38 | const categories = await server.videos.getCategories() | ||
39 | |||
40 | expect(categories[1]).to.not.exist | ||
41 | expect(categories[2]).to.not.exist | ||
42 | |||
43 | expect(categories[42]).to.equal('Best category') | ||
44 | expect(categories[43]).to.equal('High best category') | ||
45 | }) | ||
46 | |||
47 | it('Should have updated licences', async function () { | ||
48 | const licences = await server.videos.getLicences() | ||
49 | |||
50 | expect(licences[1]).to.not.exist | ||
51 | expect(licences[7]).to.not.exist | ||
52 | |||
53 | expect(licences[42]).to.equal('Best licence') | ||
54 | expect(licences[43]).to.equal('High best licence') | ||
55 | }) | ||
56 | |||
57 | it('Should have updated video privacies', async function () { | ||
58 | const privacies = await server.videos.getPrivacies() | ||
59 | |||
60 | expect(privacies[1]).to.exist | ||
61 | expect(privacies[2]).to.not.exist | ||
62 | expect(privacies[3]).to.exist | ||
63 | expect(privacies[4]).to.exist | ||
64 | }) | ||
65 | |||
66 | it('Should have updated playlist privacies', async function () { | ||
67 | const playlistPrivacies = await server.playlists.getPrivacies() | ||
68 | |||
69 | expect(playlistPrivacies[1]).to.exist | ||
70 | expect(playlistPrivacies[2]).to.exist | ||
71 | expect(playlistPrivacies[3]).to.not.exist | ||
72 | }) | ||
73 | |||
74 | it('Should not be able to create a video with this privacy', async function () { | ||
75 | const attributes = { name: 'video', privacy: VideoPrivacy.UNLISTED } | ||
76 | await server.videos.upload({ attributes, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
77 | }) | ||
78 | |||
79 | it('Should not be able to create a video with this privacy', async function () { | ||
80 | const attributes = { displayName: 'video playlist', privacy: VideoPlaylistPrivacy.PRIVATE } | ||
81 | await server.playlists.create({ attributes, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
82 | }) | ||
83 | |||
84 | it('Should be able to upload a video with these values', async function () { | ||
85 | const attributes = { name: 'video', category: 42, licence: 42, language: 'al_bhed2' } | ||
86 | const { uuid } = await server.videos.upload({ attributes }) | ||
87 | |||
88 | const video = await server.videos.get({ id: uuid }) | ||
89 | expect(video.language.label).to.equal('Al Bhed 2') | ||
90 | expect(video.licence.label).to.equal('Best licence') | ||
91 | expect(video.category.label).to.equal('Best category') | ||
92 | }) | ||
93 | |||
94 | it('Should uninstall the plugin and reset languages, categories, licences and privacies', async function () { | ||
95 | await server.plugins.uninstall({ npmName: 'peertube-plugin-test-video-constants' }) | ||
96 | |||
97 | { | ||
98 | const languages = await server.videos.getLanguages() | ||
99 | |||
100 | expect(languages['en']).to.equal('English') | ||
101 | expect(languages['fr']).to.equal('French') | ||
102 | |||
103 | expect(languages['al_bhed']).to.not.exist | ||
104 | expect(languages['al_bhed2']).to.not.exist | ||
105 | expect(languages['al_bhed3']).to.not.exist | ||
106 | } | ||
107 | |||
108 | { | ||
109 | const categories = await server.videos.getCategories() | ||
110 | |||
111 | expect(categories[1]).to.equal('Music') | ||
112 | expect(categories[2]).to.equal('Films') | ||
113 | |||
114 | expect(categories[42]).to.not.exist | ||
115 | expect(categories[43]).to.not.exist | ||
116 | } | ||
117 | |||
118 | { | ||
119 | const licences = await server.videos.getLicences() | ||
120 | |||
121 | expect(licences[1]).to.equal('Attribution') | ||
122 | expect(licences[7]).to.equal('Public Domain Dedication') | ||
123 | |||
124 | expect(licences[42]).to.not.exist | ||
125 | expect(licences[43]).to.not.exist | ||
126 | } | ||
127 | |||
128 | { | ||
129 | const privacies = await server.videos.getPrivacies() | ||
130 | |||
131 | expect(privacies[1]).to.exist | ||
132 | expect(privacies[2]).to.exist | ||
133 | expect(privacies[3]).to.exist | ||
134 | expect(privacies[4]).to.exist | ||
135 | } | ||
136 | |||
137 | { | ||
138 | const playlistPrivacies = await server.playlists.getPrivacies() | ||
139 | |||
140 | expect(playlistPrivacies[1]).to.exist | ||
141 | expect(playlistPrivacies[2]).to.exist | ||
142 | expect(playlistPrivacies[3]).to.exist | ||
143 | } | ||
144 | }) | ||
145 | |||
146 | it('Should be able to reset categories', async function () { | ||
147 | await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') }) | ||
148 | |||
149 | { | ||
150 | const categories = await server.videos.getCategories() | ||
151 | |||
152 | expect(categories[1]).to.not.exist | ||
153 | expect(categories[2]).to.not.exist | ||
154 | |||
155 | expect(categories[42]).to.exist | ||
156 | expect(categories[43]).to.exist | ||
157 | } | ||
158 | |||
159 | await makeGetRequest({ | ||
160 | url: server.url, | ||
161 | token: server.accessToken, | ||
162 | path: '/plugins/test-video-constants/router/reset-categories', | ||
163 | expectedStatus: HttpStatusCode.NO_CONTENT_204 | ||
164 | }) | ||
165 | |||
166 | { | ||
167 | const categories = await server.videos.getCategories() | ||
168 | |||
169 | expect(categories[1]).to.exist | ||
170 | expect(categories[2]).to.exist | ||
171 | |||
172 | expect(categories[42]).to.not.exist | ||
173 | expect(categories[43]).to.not.exist | ||
174 | } | ||
175 | }) | ||
176 | |||
177 | after(async function () { | ||
178 | await cleanupTests([ server ]) | ||
179 | }) | ||
180 | }) | ||
diff --git a/packages/tests/src/server-helpers/activitypub.ts b/packages/tests/src/server-helpers/activitypub.ts new file mode 100644 index 000000000..dfcd0389f --- /dev/null +++ b/packages/tests/src/server-helpers/activitypub.ts | |||
@@ -0,0 +1,176 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
4 | import { signAndContextify } from '@peertube/peertube-server/server/helpers/activity-pub-utils.js' | ||
5 | import { | ||
6 | isHTTPSignatureVerified, | ||
7 | isJsonLDSignatureVerified, | ||
8 | parseHTTPSignature | ||
9 | } from '@peertube/peertube-server/server/helpers/peertube-crypto.js' | ||
10 | import { buildRequestStub } from '@tests/shared/tests.js' | ||
11 | import { expect } from 'chai' | ||
12 | import { readJsonSync } from 'fs-extra/esm' | ||
13 | import cloneDeep from 'lodash-es/cloneDeep.js' | ||
14 | |||
15 | function fakeFilter () { | ||
16 | return (data: any) => Promise.resolve(data) | ||
17 | } | ||
18 | |||
19 | describe('Test activity pub helpers', function () { | ||
20 | |||
21 | describe('When checking the Linked Signature', function () { | ||
22 | |||
23 | it('Should fail with an invalid Mastodon signature', async function () { | ||
24 | const body = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/create-bad-signature.json')) | ||
25 | const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/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 fail with an invalid public key', async function () { | ||
34 | const body = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/create.json')) | ||
35 | const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/bad-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.false | ||
41 | }) | ||
42 | |||
43 | it('Should succeed with a valid Mastodon signature', async function () { | ||
44 | const body = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/create.json')) | ||
45 | const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey | ||
46 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } | ||
47 | |||
48 | const result = await isJsonLDSignatureVerified(fromActor as any, body) | ||
49 | |||
50 | expect(result).to.be.true | ||
51 | }) | ||
52 | |||
53 | it('Should fail with an invalid PeerTube signature', async function () { | ||
54 | const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json')) | ||
55 | const body = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) | ||
56 | |||
57 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } | ||
58 | const signedBody = await signAndContextify(actorSignature as any, body, 'Announce', fakeFilter()) | ||
59 | |||
60 | const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' } | ||
61 | const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) | ||
62 | |||
63 | expect(result).to.be.false | ||
64 | }) | ||
65 | |||
66 | it('Should succeed with a valid PeerTube signature', async function () { | ||
67 | const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/keys.json')) | ||
68 | const body = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) | ||
69 | |||
70 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } | ||
71 | const signedBody = await signAndContextify(actorSignature as any, body, 'Announce', fakeFilter()) | ||
72 | |||
73 | const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' } | ||
74 | const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) | ||
75 | |||
76 | expect(result).to.be.true | ||
77 | }) | ||
78 | }) | ||
79 | |||
80 | describe('When checking HTTP signature', function () { | ||
81 | it('Should fail with an invalid http signature', async function () { | ||
82 | const req = buildRequestStub() | ||
83 | req.method = 'POST' | ||
84 | req.url = '/accounts/ronan/inbox' | ||
85 | |||
86 | const mastodonObject = cloneDeep(readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/bad-http-signature.json'))) | ||
87 | req.body = mastodonObject.body | ||
88 | req.headers = mastodonObject.headers | ||
89 | |||
90 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | ||
91 | const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey | ||
92 | |||
93 | const actor = { publicKey } | ||
94 | const verified = isHTTPSignatureVerified(parsed, actor as any) | ||
95 | |||
96 | expect(verified).to.be.false | ||
97 | }) | ||
98 | |||
99 | it('Should fail with an invalid public key', async function () { | ||
100 | const req = buildRequestStub() | ||
101 | req.method = 'POST' | ||
102 | req.url = '/accounts/ronan/inbox' | ||
103 | |||
104 | const mastodonObject = cloneDeep(readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json'))) | ||
105 | req.body = mastodonObject.body | ||
106 | req.headers = mastodonObject.headers | ||
107 | |||
108 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | ||
109 | const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey | ||
110 | |||
111 | const actor = { publicKey } | ||
112 | const verified = isHTTPSignatureVerified(parsed, actor as any) | ||
113 | |||
114 | expect(verified).to.be.false | ||
115 | }) | ||
116 | |||
117 | it('Should fail because of clock skew', async function () { | ||
118 | const req = buildRequestStub() | ||
119 | req.method = 'POST' | ||
120 | req.url = '/accounts/ronan/inbox' | ||
121 | |||
122 | const mastodonObject = cloneDeep(readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json'))) | ||
123 | req.body = mastodonObject.body | ||
124 | req.headers = mastodonObject.headers | ||
125 | |||
126 | let errored = false | ||
127 | try { | ||
128 | parseHTTPSignature(req) | ||
129 | } catch { | ||
130 | errored = true | ||
131 | } | ||
132 | |||
133 | expect(errored).to.be.true | ||
134 | }) | ||
135 | |||
136 | it('Should with a scheme', async function () { | ||
137 | const req = buildRequestStub() | ||
138 | req.method = 'POST' | ||
139 | req.url = '/accounts/ronan/inbox' | ||
140 | |||
141 | const mastodonObject = cloneDeep(readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json'))) | ||
142 | req.body = mastodonObject.body | ||
143 | req.headers = mastodonObject.headers | ||
144 | req.headers = 'Signature ' + mastodonObject.headers | ||
145 | |||
146 | let errored = false | ||
147 | try { | ||
148 | parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | ||
149 | } catch { | ||
150 | errored = true | ||
151 | } | ||
152 | |||
153 | expect(errored).to.be.true | ||
154 | }) | ||
155 | |||
156 | it('Should succeed with a valid signature', async function () { | ||
157 | const req = buildRequestStub() | ||
158 | req.method = 'POST' | ||
159 | req.url = '/accounts/ronan/inbox' | ||
160 | |||
161 | const mastodonObject = cloneDeep(readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json'))) | ||
162 | req.body = mastodonObject.body | ||
163 | req.headers = mastodonObject.headers | ||
164 | |||
165 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | ||
166 | const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey | ||
167 | |||
168 | const actor = { publicKey } | ||
169 | const verified = isHTTPSignatureVerified(parsed, actor as any) | ||
170 | |||
171 | expect(verified).to.be.true | ||
172 | }) | ||
173 | |||
174 | }) | ||
175 | |||
176 | }) | ||
diff --git a/packages/tests/src/server-helpers/core-utils.ts b/packages/tests/src/server-helpers/core-utils.ts new file mode 100644 index 000000000..06c78591e --- /dev/null +++ b/packages/tests/src/server-helpers/core-utils.ts | |||
@@ -0,0 +1,150 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import snakeCase from 'lodash-es/snakeCase.js' | ||
5 | import validator from 'validator' | ||
6 | import { getAverageTheoreticalBitrate, getMaxTheoreticalBitrate } from '@peertube/peertube-core-utils' | ||
7 | import { VideoResolution } from '@peertube/peertube-models' | ||
8 | import { objectConverter, parseBytes, parseDurationToMs } from '@peertube/peertube-server/server/helpers/core-utils.js' | ||
9 | |||
10 | describe('Parse Bytes', function () { | ||
11 | |||
12 | it('Should pass on valid value', async function () { | ||
13 | // just return it | ||
14 | expect(parseBytes(-1024)).to.equal(-1024) | ||
15 | expect(parseBytes(1024)).to.equal(1024) | ||
16 | expect(parseBytes(1048576)).to.equal(1048576) | ||
17 | expect(parseBytes('1024')).to.equal(1024) | ||
18 | expect(parseBytes('1048576')).to.equal(1048576) | ||
19 | |||
20 | // sizes | ||
21 | expect(parseBytes('1B')).to.equal(1024) | ||
22 | expect(parseBytes('1MB')).to.equal(1048576) | ||
23 | expect(parseBytes('1GB')).to.equal(1073741824) | ||
24 | expect(parseBytes('1TB')).to.equal(1099511627776) | ||
25 | |||
26 | expect(parseBytes('5GB')).to.equal(5368709120) | ||
27 | expect(parseBytes('5TB')).to.equal(5497558138880) | ||
28 | |||
29 | expect(parseBytes('1024B')).to.equal(1048576) | ||
30 | expect(parseBytes('1024MB')).to.equal(1073741824) | ||
31 | expect(parseBytes('1024GB')).to.equal(1099511627776) | ||
32 | expect(parseBytes('1024TB')).to.equal(1125899906842624) | ||
33 | |||
34 | // with whitespace | ||
35 | expect(parseBytes('1 GB')).to.equal(1073741824) | ||
36 | expect(parseBytes('1\tGB')).to.equal(1073741824) | ||
37 | |||
38 | // sum value | ||
39 | expect(parseBytes('1TB 1024MB')).to.equal(1100585369600) | ||
40 | expect(parseBytes('4GB 1024MB')).to.equal(5368709120) | ||
41 | expect(parseBytes('4TB 1024GB')).to.equal(5497558138880) | ||
42 | expect(parseBytes('4TB 1024GB 0MB')).to.equal(5497558138880) | ||
43 | expect(parseBytes('1024TB 1024GB 1024MB')).to.equal(1127000492212224) | ||
44 | }) | ||
45 | |||
46 | it('Should be invalid when given invalid value', async function () { | ||
47 | expect(parseBytes('6GB 1GB')).to.equal(6) | ||
48 | }) | ||
49 | }) | ||
50 | |||
51 | describe('Parse duration', function () { | ||
52 | |||
53 | it('Should pass when given valid value', async function () { | ||
54 | expect(parseDurationToMs(35)).to.equal(35) | ||
55 | expect(parseDurationToMs(-35)).to.equal(-35) | ||
56 | expect(parseDurationToMs('35 seconds')).to.equal(35 * 1000) | ||
57 | expect(parseDurationToMs('1 minute')).to.equal(60 * 1000) | ||
58 | expect(parseDurationToMs('1 hour')).to.equal(3600 * 1000) | ||
59 | expect(parseDurationToMs('35 hours')).to.equal(3600 * 35 * 1000) | ||
60 | }) | ||
61 | |||
62 | it('Should be invalid when given invalid value', async function () { | ||
63 | expect(parseBytes('35m 5s')).to.equal(35) | ||
64 | }) | ||
65 | }) | ||
66 | |||
67 | describe('Object', function () { | ||
68 | |||
69 | it('Should convert an object', async function () { | ||
70 | function keyConverter (k: string) { | ||
71 | return snakeCase(k) | ||
72 | } | ||
73 | |||
74 | function valueConverter (v: any) { | ||
75 | if (validator.default.isNumeric(v + '')) return parseInt('' + v, 10) | ||
76 | |||
77 | return v | ||
78 | } | ||
79 | |||
80 | const obj = { | ||
81 | mySuperKey: 'hello', | ||
82 | mySuper2Key: '45', | ||
83 | mySuper3Key: { | ||
84 | mySuperSubKey: '15', | ||
85 | mySuperSub2Key: 'hello', | ||
86 | mySuperSub3Key: [ '1', 'hello', 2 ], | ||
87 | mySuperSub4Key: 4 | ||
88 | }, | ||
89 | mySuper4Key: 45, | ||
90 | toto: { | ||
91 | super_key: '15', | ||
92 | superKey2: 'hello' | ||
93 | }, | ||
94 | super_key: { | ||
95 | superKey4: 15 | ||
96 | } | ||
97 | } | ||
98 | |||
99 | const res = objectConverter(obj, keyConverter, valueConverter) | ||
100 | |||
101 | expect(res.my_super_key).to.equal('hello') | ||
102 | expect(res.my_super_2_key).to.equal(45) | ||
103 | expect(res.my_super_3_key.my_super_sub_key).to.equal(15) | ||
104 | expect(res.my_super_3_key.my_super_sub_2_key).to.equal('hello') | ||
105 | expect(res.my_super_3_key.my_super_sub_3_key).to.deep.equal([ 1, 'hello', 2 ]) | ||
106 | expect(res.my_super_3_key.my_super_sub_4_key).to.equal(4) | ||
107 | expect(res.toto.super_key).to.equal(15) | ||
108 | expect(res.toto.super_key_2).to.equal('hello') | ||
109 | expect(res.super_key.super_key_4).to.equal(15) | ||
110 | |||
111 | // Immutable | ||
112 | expect(res.mySuperKey).to.be.undefined | ||
113 | expect(obj['my_super_key']).to.be.undefined | ||
114 | }) | ||
115 | }) | ||
116 | |||
117 | describe('Bitrate', function () { | ||
118 | |||
119 | it('Should get appropriate max bitrate', function () { | ||
120 | const tests = [ | ||
121 | { resolution: VideoResolution.H_144P, ratio: 16 / 9, fps: 24, min: 200, max: 400 }, | ||
122 | { resolution: VideoResolution.H_240P, ratio: 16 / 9, fps: 24, min: 600, max: 800 }, | ||
123 | { resolution: VideoResolution.H_360P, ratio: 16 / 9, fps: 24, min: 1200, max: 1600 }, | ||
124 | { resolution: VideoResolution.H_480P, ratio: 16 / 9, fps: 24, min: 2000, max: 2300 }, | ||
125 | { resolution: VideoResolution.H_720P, ratio: 16 / 9, fps: 24, min: 4000, max: 4400 }, | ||
126 | { resolution: VideoResolution.H_1080P, ratio: 16 / 9, fps: 24, min: 8000, max: 10000 }, | ||
127 | { resolution: VideoResolution.H_4K, ratio: 16 / 9, fps: 24, min: 25000, max: 30000 } | ||
128 | ] | ||
129 | |||
130 | for (const test of tests) { | ||
131 | expect(getMaxTheoreticalBitrate(test)).to.be.above(test.min * 1000).and.below(test.max * 1000) | ||
132 | } | ||
133 | }) | ||
134 | |||
135 | it('Should get appropriate average bitrate', function () { | ||
136 | const tests = [ | ||
137 | { resolution: VideoResolution.H_144P, ratio: 16 / 9, fps: 24, min: 50, max: 300 }, | ||
138 | { resolution: VideoResolution.H_240P, ratio: 16 / 9, fps: 24, min: 350, max: 450 }, | ||
139 | { resolution: VideoResolution.H_360P, ratio: 16 / 9, fps: 24, min: 700, max: 900 }, | ||
140 | { resolution: VideoResolution.H_480P, ratio: 16 / 9, fps: 24, min: 1100, max: 1300 }, | ||
141 | { resolution: VideoResolution.H_720P, ratio: 16 / 9, fps: 24, min: 2300, max: 2500 }, | ||
142 | { resolution: VideoResolution.H_1080P, ratio: 16 / 9, fps: 24, min: 4700, max: 5000 }, | ||
143 | { resolution: VideoResolution.H_4K, ratio: 16 / 9, fps: 24, min: 15000, max: 17000 } | ||
144 | ] | ||
145 | |||
146 | for (const test of tests) { | ||
147 | expect(getAverageTheoreticalBitrate(test)).to.be.above(test.min * 1000).and.below(test.max * 1000) | ||
148 | } | ||
149 | }) | ||
150 | }) | ||
diff --git a/packages/tests/src/server-helpers/crypto.ts b/packages/tests/src/server-helpers/crypto.ts new file mode 100644 index 000000000..4bf5b8a45 --- /dev/null +++ b/packages/tests/src/server-helpers/crypto.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { decrypt, encrypt } from '@peertube/peertube-server/server/helpers/peertube-crypto.js' | ||
5 | |||
6 | describe('Encrypt/Descrypt', function () { | ||
7 | |||
8 | it('Should encrypt and decrypt the string', async function () { | ||
9 | const secret = 'my_secret' | ||
10 | const str = 'my super string' | ||
11 | |||
12 | const encrypted = await encrypt(str, secret) | ||
13 | const decrypted = await decrypt(encrypted, secret) | ||
14 | |||
15 | expect(str).to.equal(decrypted) | ||
16 | }) | ||
17 | |||
18 | it('Should not decrypt without the same secret', async function () { | ||
19 | const str = 'my super string' | ||
20 | |||
21 | const encrypted = await encrypt(str, 'my_secret') | ||
22 | |||
23 | let error = false | ||
24 | |||
25 | try { | ||
26 | await decrypt(encrypted, 'my_sicret') | ||
27 | } catch (err) { | ||
28 | error = true | ||
29 | } | ||
30 | |||
31 | expect(error).to.be.true | ||
32 | }) | ||
33 | }) | ||
diff --git a/packages/tests/src/server-helpers/dns.ts b/packages/tests/src/server-helpers/dns.ts new file mode 100644 index 000000000..64e3112a2 --- /dev/null +++ b/packages/tests/src/server-helpers/dns.ts | |||
@@ -0,0 +1,16 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { isResolvingToUnicastOnly } from '@peertube/peertube-server/server/helpers/dns.js' | ||
5 | |||
6 | describe('DNS helpers', function () { | ||
7 | |||
8 | it('Should correctly check unicast IPs', async function () { | ||
9 | expect(await isResolvingToUnicastOnly('cpy.re')).to.be.true | ||
10 | expect(await isResolvingToUnicastOnly('framasoft.org')).to.be.true | ||
11 | expect(await isResolvingToUnicastOnly('8.8.8.8')).to.be.true | ||
12 | |||
13 | expect(await isResolvingToUnicastOnly('127.0.0.1')).to.be.false | ||
14 | expect(await isResolvingToUnicastOnly('127.0.0.1.cpy.re')).to.be.false | ||
15 | }) | ||
16 | }) | ||
diff --git a/packages/tests/src/server-helpers/image.ts b/packages/tests/src/server-helpers/image.ts new file mode 100644 index 000000000..34675d385 --- /dev/null +++ b/packages/tests/src/server-helpers/image.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { remove } from 'fs-extra/esm' | ||
5 | import { readFile } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { buildAbsoluteFixturePath, root } from '@peertube/peertube-node-utils' | ||
8 | import { execPromise } from '@peertube/peertube-server/server/helpers/core-utils.js' | ||
9 | import { processImage } from '@peertube/peertube-server/server/helpers/image-utils.js' | ||
10 | |||
11 | async function checkBuffers (path1: string, path2: string, equals: boolean) { | ||
12 | const [ buf1, buf2 ] = await Promise.all([ | ||
13 | readFile(path1), | ||
14 | readFile(path2) | ||
15 | ]) | ||
16 | |||
17 | if (equals) { | ||
18 | expect(buf1.equals(buf2)).to.be.true | ||
19 | } else { | ||
20 | expect(buf1.equals(buf2)).to.be.false | ||
21 | } | ||
22 | } | ||
23 | |||
24 | async function hasTitleExif (path: string) { | ||
25 | const result = JSON.parse(await execPromise(`exiftool -json ${path}`)) | ||
26 | |||
27 | return result[0]?.Title === 'should be removed' | ||
28 | } | ||
29 | |||
30 | describe('Image helpers', function () { | ||
31 | const imageDestDir = join(root(), 'test-images') | ||
32 | |||
33 | const imageDestJPG = join(imageDestDir, 'test.jpg') | ||
34 | const imageDestPNG = join(imageDestDir, 'test.png') | ||
35 | |||
36 | const thumbnailSize = { width: 280, height: 157 } | ||
37 | |||
38 | it('Should skip processing if the source image is okay', async function () { | ||
39 | const input = buildAbsoluteFixturePath('custom-thumbnail.jpg') | ||
40 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | ||
41 | |||
42 | await checkBuffers(input, imageDestJPG, true) | ||
43 | }) | ||
44 | |||
45 | it('Should not skip processing if the source image does not have the appropriate extension', async function () { | ||
46 | const input = buildAbsoluteFixturePath('custom-thumbnail.png') | ||
47 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | ||
48 | |||
49 | await checkBuffers(input, imageDestJPG, false) | ||
50 | }) | ||
51 | |||
52 | it('Should not skip processing if the source image does not have the appropriate size', async function () { | ||
53 | const input = buildAbsoluteFixturePath('custom-preview.jpg') | ||
54 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | ||
55 | |||
56 | await checkBuffers(input, imageDestJPG, false) | ||
57 | }) | ||
58 | |||
59 | it('Should not skip processing if the source image does not have the appropriate size', async function () { | ||
60 | const input = buildAbsoluteFixturePath('custom-thumbnail-big.jpg') | ||
61 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | ||
62 | |||
63 | await checkBuffers(input, imageDestJPG, false) | ||
64 | }) | ||
65 | |||
66 | it('Should strip exif for a jpg file that can not be copied', async function () { | ||
67 | const input = buildAbsoluteFixturePath('exif.jpg') | ||
68 | expect(await hasTitleExif(input)).to.be.true | ||
69 | |||
70 | await processImage({ path: input, destination: imageDestJPG, newSize: { width: 100, height: 100 }, keepOriginal: true }) | ||
71 | await checkBuffers(input, imageDestJPG, false) | ||
72 | |||
73 | expect(await hasTitleExif(imageDestJPG)).to.be.false | ||
74 | }) | ||
75 | |||
76 | it('Should strip exif for a jpg file that could be copied', async function () { | ||
77 | const input = buildAbsoluteFixturePath('exif.jpg') | ||
78 | expect(await hasTitleExif(input)).to.be.true | ||
79 | |||
80 | await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true }) | ||
81 | await checkBuffers(input, imageDestJPG, false) | ||
82 | |||
83 | expect(await hasTitleExif(imageDestJPG)).to.be.false | ||
84 | }) | ||
85 | |||
86 | it('Should strip exif for png', async function () { | ||
87 | const input = buildAbsoluteFixturePath('exif.png') | ||
88 | expect(await hasTitleExif(input)).to.be.true | ||
89 | |||
90 | await processImage({ path: input, destination: imageDestPNG, newSize: thumbnailSize, keepOriginal: true }) | ||
91 | expect(await hasTitleExif(imageDestPNG)).to.be.false | ||
92 | }) | ||
93 | |||
94 | after(async function () { | ||
95 | await remove(imageDestDir) | ||
96 | }) | ||
97 | }) | ||
diff --git a/packages/tests/src/server-helpers/index.ts b/packages/tests/src/server-helpers/index.ts new file mode 100644 index 000000000..04a26560c --- /dev/null +++ b/packages/tests/src/server-helpers/index.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import './activitypub.js' | ||
2 | import './core-utils.js' | ||
3 | import './crypto.js' | ||
4 | import './dns.js' | ||
5 | import './image.js' | ||
6 | import './markdown.js' | ||
7 | import './mentions.js' | ||
8 | import './request.js' | ||
9 | import './validator.js' | ||
10 | import './version.js' | ||
diff --git a/packages/tests/src/server-helpers/markdown.ts b/packages/tests/src/server-helpers/markdown.ts new file mode 100644 index 000000000..96e3c34dc --- /dev/null +++ b/packages/tests/src/server-helpers/markdown.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { mdToOneLinePlainText } from '@peertube/peertube-server/server/helpers/markdown.js' | ||
4 | import { expect } from 'chai' | ||
5 | |||
6 | describe('Markdown helpers', function () { | ||
7 | |||
8 | describe('Plain text', function () { | ||
9 | |||
10 | it('Should convert a list to plain text', function () { | ||
11 | const result = mdToOneLinePlainText(`* list 1 | ||
12 | * list 2 | ||
13 | * list 3`) | ||
14 | |||
15 | expect(result).to.equal('list 1, list 2, list 3') | ||
16 | }) | ||
17 | |||
18 | it('Should convert a list with indentation to plain text', function () { | ||
19 | const result = mdToOneLinePlainText(`Hello: | ||
20 | * list 1 | ||
21 | * list 2 | ||
22 | * list 3`) | ||
23 | |||
24 | expect(result).to.equal('Hello: list 1, list 2, list 3') | ||
25 | }) | ||
26 | |||
27 | it('Should convert HTML to plain text', function () { | ||
28 | const result = mdToOneLinePlainText(`**Hello** <strong>coucou</strong>`) | ||
29 | |||
30 | expect(result).to.equal('Hello coucou') | ||
31 | }) | ||
32 | |||
33 | it('Should convert tags to plain text', function () { | ||
34 | const result = mdToOneLinePlainText(`#déconversion\n#newage\n#histoire`) | ||
35 | |||
36 | expect(result).to.equal('#déconversion #newage #histoire') | ||
37 | }) | ||
38 | }) | ||
39 | }) | ||
diff --git a/packages/tests/src/server-helpers/mentions.ts b/packages/tests/src/server-helpers/mentions.ts new file mode 100644 index 000000000..153931d60 --- /dev/null +++ b/packages/tests/src/server-helpers/mentions.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { extractMentions } from '@peertube/peertube-server/server/helpers/mentions.js' | ||
5 | |||
6 | describe('Comment model', function () { | ||
7 | it('Should correctly extract mentions', async function () { | ||
8 | const text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' + | ||
9 | 'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end' | ||
10 | |||
11 | const isOwned = true | ||
12 | |||
13 | const result = extractMentions(text, isOwned).sort((a, b) => a.localeCompare(b)) | ||
14 | |||
15 | expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ]) | ||
16 | }) | ||
17 | }) | ||
diff --git a/packages/tests/src/server-helpers/request.ts b/packages/tests/src/server-helpers/request.ts new file mode 100644 index 000000000..f4b9af52e --- /dev/null +++ b/packages/tests/src/server-helpers/request.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists, remove } from 'fs-extra/esm' | ||
5 | import { join } from 'path' | ||
6 | import { wait } from '@peertube/peertube-core-utils' | ||
7 | import { root } from '@peertube/peertube-node-utils' | ||
8 | import { doRequest, doRequestAndSaveToFile } from '@peertube/peertube-server/server/helpers/requests.js' | ||
9 | import { Mock429 } from '@tests/shared/mock-servers/mock-429.js' | ||
10 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
11 | |||
12 | describe('Request helpers', function () { | ||
13 | const destPath1 = join(root(), 'test-output-1.txt') | ||
14 | const destPath2 = join(root(), 'test-output-2.txt') | ||
15 | |||
16 | it('Should throw an error when the bytes limit is exceeded for request', async function () { | ||
17 | try { | ||
18 | await doRequest(FIXTURE_URLS.file4K, { bodyKBLimit: 3 }) | ||
19 | } catch { | ||
20 | return | ||
21 | } | ||
22 | |||
23 | throw new Error('No error thrown by do request') | ||
24 | }) | ||
25 | |||
26 | it('Should throw an error when the bytes limit is exceeded for request and save file', async function () { | ||
27 | try { | ||
28 | await doRequestAndSaveToFile(FIXTURE_URLS.file4K, destPath1, { bodyKBLimit: 3 }) | ||
29 | } catch { | ||
30 | |||
31 | await wait(500) | ||
32 | expect(await pathExists(destPath1)).to.be.false | ||
33 | return | ||
34 | } | ||
35 | |||
36 | throw new Error('No error thrown by do request and save to file') | ||
37 | }) | ||
38 | |||
39 | it('Should correctly retry on 429 error', async function () { | ||
40 | this.timeout(25000) | ||
41 | |||
42 | const mock = new Mock429() | ||
43 | const port = await mock.initialize() | ||
44 | |||
45 | const before = new Date().getTime() | ||
46 | await doRequest('http://127.0.0.1:' + port) | ||
47 | |||
48 | expect(new Date().getTime() - before).to.be.greaterThan(2000) | ||
49 | |||
50 | await mock.terminate() | ||
51 | }) | ||
52 | |||
53 | it('Should succeed if the file is below the limit', async function () { | ||
54 | await doRequest(FIXTURE_URLS.file4K, { bodyKBLimit: 5 }) | ||
55 | await doRequestAndSaveToFile(FIXTURE_URLS.file4K, destPath2, { bodyKBLimit: 5 }) | ||
56 | |||
57 | expect(await pathExists(destPath2)).to.be.true | ||
58 | }) | ||
59 | |||
60 | after(async function () { | ||
61 | await remove(destPath1) | ||
62 | await remove(destPath2) | ||
63 | }) | ||
64 | }) | ||
diff --git a/packages/tests/src/server-helpers/validator.ts b/packages/tests/src/server-helpers/validator.ts new file mode 100644 index 000000000..792bd501c --- /dev/null +++ b/packages/tests/src/server-helpers/validator.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | isPluginStableOrUnstableVersionValid, | ||
6 | isPluginStableVersionValid | ||
7 | } from '@peertube/peertube-server/server/helpers/custom-validators/plugins.js' | ||
8 | |||
9 | describe('Validators', function () { | ||
10 | |||
11 | it('Should correctly check stable plugin versions', async function () { | ||
12 | expect(isPluginStableVersionValid('3.4.0')).to.be.true | ||
13 | expect(isPluginStableVersionValid('0.4.0')).to.be.true | ||
14 | expect(isPluginStableVersionValid('0.1.0')).to.be.true | ||
15 | |||
16 | expect(isPluginStableVersionValid('0.1.0-beta-1')).to.be.false | ||
17 | expect(isPluginStableVersionValid('hello')).to.be.false | ||
18 | expect(isPluginStableVersionValid('0.x.a')).to.be.false | ||
19 | }) | ||
20 | |||
21 | it('Should correctly check unstable plugin versions', async function () { | ||
22 | expect(isPluginStableOrUnstableVersionValid('3.4.0')).to.be.true | ||
23 | expect(isPluginStableOrUnstableVersionValid('0.4.0')).to.be.true | ||
24 | expect(isPluginStableOrUnstableVersionValid('0.1.0')).to.be.true | ||
25 | |||
26 | expect(isPluginStableOrUnstableVersionValid('0.1.0-beta.1')).to.be.true | ||
27 | expect(isPluginStableOrUnstableVersionValid('0.1.0-alpha.45')).to.be.true | ||
28 | expect(isPluginStableOrUnstableVersionValid('0.1.0-rc.45')).to.be.true | ||
29 | |||
30 | expect(isPluginStableOrUnstableVersionValid('hello')).to.be.false | ||
31 | expect(isPluginStableOrUnstableVersionValid('0.x.a')).to.be.false | ||
32 | expect(isPluginStableOrUnstableVersionValid('0.1.0-rc-45')).to.be.false | ||
33 | expect(isPluginStableOrUnstableVersionValid('0.1.0-rc.45d')).to.be.false | ||
34 | }) | ||
35 | }) | ||
diff --git a/packages/tests/src/server-helpers/version.ts b/packages/tests/src/server-helpers/version.ts new file mode 100644 index 000000000..76892d1e7 --- /dev/null +++ b/packages/tests/src/server-helpers/version.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { compareSemVer } from '@peertube/peertube-core-utils' | ||
5 | |||
6 | describe('Version', function () { | ||
7 | |||
8 | it('Should correctly compare two stable versions', async function () { | ||
9 | expect(compareSemVer('3.4.0', '3.5.0')).to.be.below(0) | ||
10 | expect(compareSemVer('3.5.0', '3.4.0')).to.be.above(0) | ||
11 | |||
12 | expect(compareSemVer('3.4.0', '4.1.0')).to.be.below(0) | ||
13 | expect(compareSemVer('4.1.0', '3.4.0')).to.be.above(0) | ||
14 | |||
15 | expect(compareSemVer('3.4.0', '3.4.1')).to.be.below(0) | ||
16 | expect(compareSemVer('3.4.1', '3.4.0')).to.be.above(0) | ||
17 | }) | ||
18 | |||
19 | it('Should correctly compare two unstable version', async function () { | ||
20 | expect(compareSemVer('3.4.0-alpha', '3.4.0-beta.1')).to.be.below(0) | ||
21 | expect(compareSemVer('3.4.0-alpha.1', '3.4.0-beta.1')).to.be.below(0) | ||
22 | expect(compareSemVer('3.4.0-beta.1', '3.4.0-beta.2')).to.be.below(0) | ||
23 | expect(compareSemVer('3.4.0-beta.1', '3.5.0-alpha.1')).to.be.below(0) | ||
24 | |||
25 | expect(compareSemVer('3.4.0-alpha.1', '3.4.0-nightly.4')).to.be.below(0) | ||
26 | expect(compareSemVer('3.4.0-nightly.3', '3.4.0-nightly.4')).to.be.below(0) | ||
27 | expect(compareSemVer('3.3.0-nightly.5', '3.4.0-nightly.4')).to.be.below(0) | ||
28 | }) | ||
29 | |||
30 | it('Should correctly compare a stable and unstable versions', async function () { | ||
31 | expect(compareSemVer('3.4.0', '3.4.1-beta.1')).to.be.below(0) | ||
32 | expect(compareSemVer('3.4.0-beta.1', '3.4.0-beta.2')).to.be.below(0) | ||
33 | expect(compareSemVer('3.4.0-beta.1', '3.4.0')).to.be.below(0) | ||
34 | expect(compareSemVer('3.4.0-nightly.4', '3.4.0')).to.be.below(0) | ||
35 | }) | ||
36 | }) | ||
diff --git a/packages/tests/src/server-lib/index.ts b/packages/tests/src/server-lib/index.ts new file mode 100644 index 000000000..873f53e15 --- /dev/null +++ b/packages/tests/src/server-lib/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './video-constant-registry-factory.js' | |||
diff --git a/packages/tests/src/server-lib/video-constant-registry-factory.ts b/packages/tests/src/server-lib/video-constant-registry-factory.ts new file mode 100644 index 000000000..6bf2d1db6 --- /dev/null +++ b/packages/tests/src/server-lib/video-constant-registry-factory.ts | |||
@@ -0,0 +1,151 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions */ | ||
2 | import { expect } from 'chai' | ||
3 | import { VideoPlaylistPrivacyType, VideoPrivacyType } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | VIDEO_CATEGORIES, | ||
6 | VIDEO_LANGUAGES, | ||
7 | VIDEO_LICENCES, | ||
8 | VIDEO_PLAYLIST_PRIVACIES, | ||
9 | VIDEO_PRIVACIES | ||
10 | } from '@peertube/peertube-server/server/initializers/constants.js' | ||
11 | import { VideoConstantManagerFactory } from '@peertube/peertube-server/server/lib/plugins/video-constant-manager-factory.js' | ||
12 | |||
13 | describe('VideoConstantManagerFactory', function () { | ||
14 | const factory = new VideoConstantManagerFactory('peertube-plugin-constants') | ||
15 | |||
16 | afterEach(() => { | ||
17 | factory.resetVideoConstants('peertube-plugin-constants') | ||
18 | }) | ||
19 | |||
20 | describe('VideoCategoryManager', () => { | ||
21 | const videoCategoryManager = factory.createVideoConstantManager<number>('category') | ||
22 | |||
23 | it('Should be able to list all video category constants', () => { | ||
24 | const constants = videoCategoryManager.getConstants() | ||
25 | expect(constants).to.deep.equal(VIDEO_CATEGORIES) | ||
26 | }) | ||
27 | |||
28 | it('Should be able to delete a video category constant', () => { | ||
29 | const successfullyDeleted = videoCategoryManager.deleteConstant(1) | ||
30 | expect(successfullyDeleted).to.be.true | ||
31 | expect(videoCategoryManager.getConstantValue(1)).to.be.undefined | ||
32 | }) | ||
33 | |||
34 | it('Should be able to add a video category constant', () => { | ||
35 | const successfullyAdded = videoCategoryManager.addConstant(42, 'The meaning of life') | ||
36 | expect(successfullyAdded).to.be.true | ||
37 | expect(videoCategoryManager.getConstantValue(42)).to.equal('The meaning of life') | ||
38 | }) | ||
39 | |||
40 | it('Should be able to reset video category constants', () => { | ||
41 | videoCategoryManager.deleteConstant(1) | ||
42 | videoCategoryManager.resetConstants() | ||
43 | expect(videoCategoryManager.getConstantValue(1)).not.be.undefined | ||
44 | }) | ||
45 | }) | ||
46 | |||
47 | describe('VideoLicenceManager', () => { | ||
48 | const videoLicenceManager = factory.createVideoConstantManager<number>('licence') | ||
49 | it('Should be able to list all video licence constants', () => { | ||
50 | const constants = videoLicenceManager.getConstants() | ||
51 | expect(constants).to.deep.equal(VIDEO_LICENCES) | ||
52 | }) | ||
53 | |||
54 | it('Should be able to delete a video licence constant', () => { | ||
55 | const successfullyDeleted = videoLicenceManager.deleteConstant(1) | ||
56 | expect(successfullyDeleted).to.be.true | ||
57 | expect(videoLicenceManager.getConstantValue(1)).to.be.undefined | ||
58 | }) | ||
59 | |||
60 | it('Should be able to add a video licence constant', () => { | ||
61 | const successfullyAdded = videoLicenceManager.addConstant(42, 'European Union Public Licence') | ||
62 | expect(successfullyAdded).to.be.true | ||
63 | expect(videoLicenceManager.getConstantValue(42 as any)).to.equal('European Union Public Licence') | ||
64 | }) | ||
65 | |||
66 | it('Should be able to reset video licence constants', () => { | ||
67 | videoLicenceManager.deleteConstant(1) | ||
68 | videoLicenceManager.resetConstants() | ||
69 | expect(videoLicenceManager.getConstantValue(1)).not.be.undefined | ||
70 | }) | ||
71 | }) | ||
72 | |||
73 | describe('PlaylistPrivacyManager', () => { | ||
74 | const playlistPrivacyManager = factory.createVideoConstantManager<VideoPlaylistPrivacyType>('playlistPrivacy') | ||
75 | it('Should be able to list all video playlist privacy constants', () => { | ||
76 | const constants = playlistPrivacyManager.getConstants() | ||
77 | expect(constants).to.deep.equal(VIDEO_PLAYLIST_PRIVACIES) | ||
78 | }) | ||
79 | |||
80 | it('Should be able to delete a video playlist privacy constant', () => { | ||
81 | const successfullyDeleted = playlistPrivacyManager.deleteConstant(1) | ||
82 | expect(successfullyDeleted).to.be.true | ||
83 | expect(playlistPrivacyManager.getConstantValue(1)).to.be.undefined | ||
84 | }) | ||
85 | |||
86 | it('Should be able to add a video playlist privacy constant', () => { | ||
87 | const successfullyAdded = playlistPrivacyManager.addConstant(42 as any, 'Friends only') | ||
88 | expect(successfullyAdded).to.be.true | ||
89 | expect(playlistPrivacyManager.getConstantValue(42 as any)).to.equal('Friends only') | ||
90 | }) | ||
91 | |||
92 | it('Should be able to reset video playlist privacy constants', () => { | ||
93 | playlistPrivacyManager.deleteConstant(1) | ||
94 | playlistPrivacyManager.resetConstants() | ||
95 | expect(playlistPrivacyManager.getConstantValue(1)).not.be.undefined | ||
96 | }) | ||
97 | }) | ||
98 | |||
99 | describe('VideoPrivacyManager', () => { | ||
100 | const videoPrivacyManager = factory.createVideoConstantManager<VideoPrivacyType>('privacy') | ||
101 | it('Should be able to list all video privacy constants', () => { | ||
102 | const constants = videoPrivacyManager.getConstants() | ||
103 | expect(constants).to.deep.equal(VIDEO_PRIVACIES) | ||
104 | }) | ||
105 | |||
106 | it('Should be able to delete a video privacy constant', () => { | ||
107 | const successfullyDeleted = videoPrivacyManager.deleteConstant(1) | ||
108 | expect(successfullyDeleted).to.be.true | ||
109 | expect(videoPrivacyManager.getConstantValue(1)).to.be.undefined | ||
110 | }) | ||
111 | |||
112 | it('Should be able to add a video privacy constant', () => { | ||
113 | const successfullyAdded = videoPrivacyManager.addConstant(42 as any, 'Friends only') | ||
114 | expect(successfullyAdded).to.be.true | ||
115 | expect(videoPrivacyManager.getConstantValue(42 as any)).to.equal('Friends only') | ||
116 | }) | ||
117 | |||
118 | it('Should be able to reset video privacy constants', () => { | ||
119 | videoPrivacyManager.deleteConstant(1) | ||
120 | videoPrivacyManager.resetConstants() | ||
121 | expect(videoPrivacyManager.getConstantValue(1)).not.be.undefined | ||
122 | }) | ||
123 | }) | ||
124 | |||
125 | describe('VideoLanguageManager', () => { | ||
126 | const videoLanguageManager = factory.createVideoConstantManager<string>('language') | ||
127 | it('Should be able to list all video language constants', () => { | ||
128 | const constants = videoLanguageManager.getConstants() | ||
129 | expect(constants).to.deep.equal(VIDEO_LANGUAGES) | ||
130 | }) | ||
131 | |||
132 | it('Should be able to add a video language constant', () => { | ||
133 | const successfullyAdded = videoLanguageManager.addConstant('fr', 'Fr occitan') | ||
134 | expect(successfullyAdded).to.be.true | ||
135 | expect(videoLanguageManager.getConstantValue('fr')).to.equal('Fr occitan') | ||
136 | }) | ||
137 | |||
138 | it('Should be able to delete a video language constant', () => { | ||
139 | videoLanguageManager.addConstant('fr', 'Fr occitan') | ||
140 | const successfullyDeleted = videoLanguageManager.deleteConstant('fr') | ||
141 | expect(successfullyDeleted).to.be.true | ||
142 | expect(videoLanguageManager.getConstantValue('fr')).to.be.undefined | ||
143 | }) | ||
144 | |||
145 | it('Should be able to reset video language constants', () => { | ||
146 | videoLanguageManager.addConstant('fr', 'Fr occitan') | ||
147 | videoLanguageManager.resetConstants() | ||
148 | expect(videoLanguageManager.getConstantValue('fr')).to.be.undefined | ||
149 | }) | ||
150 | }) | ||
151 | }) | ||
diff --git a/packages/tests/src/shared/actors.ts b/packages/tests/src/shared/actors.ts new file mode 100644 index 000000000..02d507a49 --- /dev/null +++ b/packages/tests/src/shared/actors.ts | |||
@@ -0,0 +1,70 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { readdir } from 'fs/promises' | ||
6 | import { Account, VideoChannel } from '@peertube/peertube-models' | ||
7 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | async function expectChannelsFollows (options: { | ||
10 | server: PeerTubeServer | ||
11 | handle: string | ||
12 | followers: number | ||
13 | following: number | ||
14 | }) { | ||
15 | const { server } = options | ||
16 | const { data } = await server.channels.list() | ||
17 | |||
18 | return expectActorFollow({ ...options, data }) | ||
19 | } | ||
20 | |||
21 | async function expectAccountFollows (options: { | ||
22 | server: PeerTubeServer | ||
23 | handle: string | ||
24 | followers: number | ||
25 | following: number | ||
26 | }) { | ||
27 | const { server } = options | ||
28 | const { data } = await server.accounts.list() | ||
29 | |||
30 | return expectActorFollow({ ...options, data }) | ||
31 | } | ||
32 | |||
33 | async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) { | ||
34 | for (const directory of [ 'avatars' ]) { | ||
35 | const directoryPath = server.getDirectoryPath(directory) | ||
36 | |||
37 | const directoryExists = await pathExists(directoryPath) | ||
38 | expect(directoryExists).to.be.true | ||
39 | |||
40 | const files = await readdir(directoryPath) | ||
41 | for (const file of files) { | ||
42 | expect(file).to.not.contain(filename) | ||
43 | } | ||
44 | } | ||
45 | } | ||
46 | |||
47 | export { | ||
48 | expectAccountFollows, | ||
49 | expectChannelsFollows, | ||
50 | checkActorFilesWereRemoved | ||
51 | } | ||
52 | |||
53 | // --------------------------------------------------------------------------- | ||
54 | |||
55 | function expectActorFollow (options: { | ||
56 | server: PeerTubeServer | ||
57 | data: (Account | VideoChannel)[] | ||
58 | handle: string | ||
59 | followers: number | ||
60 | following: number | ||
61 | }) { | ||
62 | const { server, data, handle, followers, following } = options | ||
63 | |||
64 | const actor = data.find(a => a.name + '@' + a.host === handle) | ||
65 | const message = `${handle} on ${server.url}` | ||
66 | |||
67 | expect(actor, message).to.exist | ||
68 | expect(actor.followersCount).to.equal(followers, message) | ||
69 | expect(actor.followingCount).to.equal(following, message) | ||
70 | } | ||
diff --git a/packages/tests/src/shared/captions.ts b/packages/tests/src/shared/captions.ts new file mode 100644 index 000000000..436cf8dcc --- /dev/null +++ b/packages/tests/src/shared/captions.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import request from 'supertest' | ||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | |||
5 | async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) { | ||
6 | const res = await request(url) | ||
7 | .get(captionPath) | ||
8 | .expect(HttpStatusCode.OK_200) | ||
9 | |||
10 | if (toTest instanceof RegExp) { | ||
11 | expect(res.text).to.match(toTest) | ||
12 | } else { | ||
13 | expect(res.text).to.contain(toTest) | ||
14 | } | ||
15 | } | ||
16 | |||
17 | // --------------------------------------------------------------------------- | ||
18 | |||
19 | export { | ||
20 | testCaptionFile | ||
21 | } | ||
diff --git a/packages/tests/src/shared/checks.ts b/packages/tests/src/shared/checks.ts new file mode 100644 index 000000000..fea618a30 --- /dev/null +++ b/packages/tests/src/shared/checks.ts | |||
@@ -0,0 +1,177 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { readFile } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
8 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
9 | import { makeGetRequest, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
10 | |||
11 | // Default interval -> 5 minutes | ||
12 | function dateIsValid (dateString: string | Date, interval = 300000) { | ||
13 | const dateToCheck = new Date(dateString) | ||
14 | const now = new Date() | ||
15 | |||
16 | return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval | ||
17 | } | ||
18 | |||
19 | function expectStartWith (str: string, start: string) { | ||
20 | expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.true | ||
21 | } | ||
22 | |||
23 | function expectNotStartWith (str: string, start: string) { | ||
24 | expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.false | ||
25 | } | ||
26 | |||
27 | function expectEndWith (str: string, end: string) { | ||
28 | expect(str.endsWith(end), `${str} does not end with ${end}`).to.be.true | ||
29 | } | ||
30 | |||
31 | // --------------------------------------------------------------------------- | ||
32 | |||
33 | async function expectLogDoesNotContain (server: PeerTubeServer, str: string) { | ||
34 | const content = await server.servers.getLogContent() | ||
35 | |||
36 | expect(content.toString()).to.not.contain(str) | ||
37 | } | ||
38 | |||
39 | async function expectLogContain (server: PeerTubeServer, str: string) { | ||
40 | const content = await server.servers.getLogContent() | ||
41 | |||
42 | expect(content.toString()).to.contain(str) | ||
43 | } | ||
44 | |||
45 | async function testImageSize (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { | ||
46 | const res = await makeGetRequest({ | ||
47 | url, | ||
48 | path: imageHTTPPath, | ||
49 | expectedStatus: HttpStatusCode.OK_200 | ||
50 | }) | ||
51 | |||
52 | const body = res.body | ||
53 | |||
54 | const data = await readFile(buildAbsoluteFixturePath(imageName + extension)) | ||
55 | const minLength = data.length - ((40 * data.length) / 100) | ||
56 | const maxLength = data.length + ((40 * data.length) / 100) | ||
57 | |||
58 | expect(body.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture') | ||
59 | expect(body.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture') | ||
60 | } | ||
61 | |||
62 | async function testImageGeneratedByFFmpeg (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { | ||
63 | if (process.env.ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS !== 'true') { | ||
64 | console.log( | ||
65 | 'Pixel comparison of image generated by ffmpeg is disabled. ' + | ||
66 | 'You can enable it using `ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS=true env variable') | ||
67 | return | ||
68 | } | ||
69 | |||
70 | return testImage(url, imageName, imageHTTPPath, extension) | ||
71 | } | ||
72 | |||
73 | async function testImage (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') { | ||
74 | const res = await makeGetRequest({ | ||
75 | url, | ||
76 | path: imageHTTPPath, | ||
77 | expectedStatus: HttpStatusCode.OK_200 | ||
78 | }) | ||
79 | |||
80 | const body = res.body | ||
81 | const data = await readFile(buildAbsoluteFixturePath(imageName + extension)) | ||
82 | |||
83 | const { PNG } = await import('pngjs') | ||
84 | const JPEG = await import('jpeg-js') | ||
85 | const pixelmatch = (await import('pixelmatch')).default | ||
86 | |||
87 | const img1 = imageHTTPPath.endsWith('.png') | ||
88 | ? PNG.sync.read(body) | ||
89 | : JPEG.decode(body) | ||
90 | |||
91 | const img2 = extension === '.png' | ||
92 | ? PNG.sync.read(data) | ||
93 | : JPEG.decode(data) | ||
94 | |||
95 | const result = pixelmatch(img1.data, img2.data, null, img1.width, img1.height, { threshold: 0.1 }) | ||
96 | |||
97 | expect(result).to.equal(0, `${imageHTTPPath} image is not the same as ${imageName}${extension}`) | ||
98 | } | ||
99 | |||
100 | async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) { | ||
101 | const base = server.servers.buildDirectory(directory) | ||
102 | |||
103 | expect(await pathExists(join(base, filePath))).to.equal(exist) | ||
104 | } | ||
105 | |||
106 | // --------------------------------------------------------------------------- | ||
107 | |||
108 | function checkBadStartPagination (url: string, path: string, token?: string, query = {}) { | ||
109 | return makeGetRequest({ | ||
110 | url, | ||
111 | path, | ||
112 | token, | ||
113 | query: { ...query, start: 'hello' }, | ||
114 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
115 | }) | ||
116 | } | ||
117 | |||
118 | async function checkBadCountPagination (url: string, path: string, token?: string, query = {}) { | ||
119 | await makeGetRequest({ | ||
120 | url, | ||
121 | path, | ||
122 | token, | ||
123 | query: { ...query, count: 'hello' }, | ||
124 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
125 | }) | ||
126 | |||
127 | await makeGetRequest({ | ||
128 | url, | ||
129 | path, | ||
130 | token, | ||
131 | query: { ...query, count: 2000 }, | ||
132 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
133 | }) | ||
134 | } | ||
135 | |||
136 | function checkBadSortPagination (url: string, path: string, token?: string, query = {}) { | ||
137 | return makeGetRequest({ | ||
138 | url, | ||
139 | path, | ||
140 | token, | ||
141 | query: { ...query, sort: 'hello' }, | ||
142 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | // --------------------------------------------------------------------------- | ||
147 | |||
148 | async function checkVideoDuration (server: PeerTubeServer, videoUUID: string, duration: number) { | ||
149 | const video = await server.videos.get({ id: videoUUID }) | ||
150 | |||
151 | expect(video.duration).to.be.approximately(duration, 1) | ||
152 | |||
153 | for (const file of video.files) { | ||
154 | const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl }) | ||
155 | |||
156 | for (const stream of metadata.streams) { | ||
157 | expect(Math.round(stream.duration)).to.be.approximately(duration, 1) | ||
158 | } | ||
159 | } | ||
160 | } | ||
161 | |||
162 | export { | ||
163 | dateIsValid, | ||
164 | testImageGeneratedByFFmpeg, | ||
165 | testImageSize, | ||
166 | testImage, | ||
167 | expectLogDoesNotContain, | ||
168 | testFileExistsOrNot, | ||
169 | expectStartWith, | ||
170 | expectNotStartWith, | ||
171 | expectEndWith, | ||
172 | checkBadStartPagination, | ||
173 | checkBadCountPagination, | ||
174 | checkBadSortPagination, | ||
175 | checkVideoDuration, | ||
176 | expectLogContain | ||
177 | } | ||
diff --git a/packages/tests/src/shared/directories.ts b/packages/tests/src/shared/directories.ts new file mode 100644 index 000000000..f21e7b7c6 --- /dev/null +++ b/packages/tests/src/shared/directories.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { readdir } from 'fs/promises' | ||
6 | import { homedir } from 'os' | ||
7 | import { join } from 'path' | ||
8 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
9 | import { PeerTubeRunnerProcess } from './peertube-runner-process.js' | ||
10 | |||
11 | export async function checkTmpIsEmpty (server: PeerTubeServer) { | ||
12 | await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ]) | ||
13 | |||
14 | if (await pathExists(server.getDirectoryPath('tmp/hls'))) { | ||
15 | await checkDirectoryIsEmpty(server, 'tmp/hls') | ||
16 | } | ||
17 | } | ||
18 | |||
19 | export async function checkPersistentTmpIsEmpty (server: PeerTubeServer) { | ||
20 | await checkDirectoryIsEmpty(server, 'tmp-persistent') | ||
21 | } | ||
22 | |||
23 | export async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) { | ||
24 | const directoryPath = server.getDirectoryPath(directory) | ||
25 | |||
26 | const directoryExists = await pathExists(directoryPath) | ||
27 | expect(directoryExists).to.be.true | ||
28 | |||
29 | const files = await readdir(directoryPath) | ||
30 | const filtered = files.filter(f => exceptions.includes(f) === false) | ||
31 | |||
32 | expect(filtered).to.have.lengthOf(0) | ||
33 | } | ||
34 | |||
35 | export async function checkPeerTubeRunnerCacheIsEmpty (runner: PeerTubeRunnerProcess) { | ||
36 | const directoryPath = join(homedir(), '.cache', 'peertube-runner-nodejs', runner.getId(), 'transcoding') | ||
37 | |||
38 | const directoryExists = await pathExists(directoryPath) | ||
39 | expect(directoryExists).to.be.true | ||
40 | |||
41 | const files = await readdir(directoryPath) | ||
42 | |||
43 | expect(files, 'Directory content: ' + files.join(', ')).to.have.lengthOf(0) | ||
44 | } | ||
diff --git a/packages/tests/src/shared/generate.ts b/packages/tests/src/shared/generate.ts new file mode 100644 index 000000000..ab2ecaf40 --- /dev/null +++ b/packages/tests/src/shared/generate.ts | |||
@@ -0,0 +1,79 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { ensureDir, pathExists } from 'fs-extra/esm' | ||
3 | import { dirname } from 'path' | ||
4 | import { getMaxTheoreticalBitrate } from '@peertube/peertube-core-utils' | ||
5 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
6 | import { getVideoStreamBitrate, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg' | ||
7 | |||
8 | async function ensureHasTooBigBitrate (fixturePath: string) { | ||
9 | const bitrate = await getVideoStreamBitrate(fixturePath) | ||
10 | const dataResolution = await getVideoStreamDimensionsInfo(fixturePath) | ||
11 | const fps = await getVideoStreamFPS(fixturePath) | ||
12 | |||
13 | const maxBitrate = getMaxTheoreticalBitrate({ ...dataResolution, fps }) | ||
14 | expect(bitrate).to.be.above(maxBitrate) | ||
15 | } | ||
16 | |||
17 | async function generateHighBitrateVideo () { | ||
18 | const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) | ||
19 | |||
20 | await ensureDir(dirname(tempFixturePath)) | ||
21 | |||
22 | const exists = await pathExists(tempFixturePath) | ||
23 | |||
24 | if (!exists) { | ||
25 | const ffmpeg = (await import('fluent-ffmpeg')).default | ||
26 | |||
27 | console.log('Generating high bitrate video.') | ||
28 | |||
29 | // Generate a random, high bitrate video on the fly, so we don't have to include | ||
30 | // a large file in the repo. The video needs to have a certain minimum length so | ||
31 | // that FFmpeg properly applies bitrate limits. | ||
32 | // https://stackoverflow.com/a/15795112 | ||
33 | return new Promise<string>((res, rej) => { | ||
34 | ffmpeg() | ||
35 | .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ]) | ||
36 | .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) | ||
37 | .outputOptions([ '-maxrate 10M', '-bufsize 10M' ]) | ||
38 | .output(tempFixturePath) | ||
39 | .on('error', rej) | ||
40 | .on('end', () => res(tempFixturePath)) | ||
41 | .run() | ||
42 | }) | ||
43 | } | ||
44 | |||
45 | await ensureHasTooBigBitrate(tempFixturePath) | ||
46 | |||
47 | return tempFixturePath | ||
48 | } | ||
49 | |||
50 | async function generateVideoWithFramerate (fps = 60) { | ||
51 | const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true) | ||
52 | |||
53 | await ensureDir(dirname(tempFixturePath)) | ||
54 | |||
55 | const exists = await pathExists(tempFixturePath) | ||
56 | if (!exists) { | ||
57 | const ffmpeg = (await import('fluent-ffmpeg')).default | ||
58 | |||
59 | console.log('Generating video with framerate %d.', fps) | ||
60 | |||
61 | return new Promise<string>((res, rej) => { | ||
62 | ffmpeg() | ||
63 | .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ]) | ||
64 | .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) | ||
65 | .outputOptions([ `-r ${fps}` ]) | ||
66 | .output(tempFixturePath) | ||
67 | .on('error', rej) | ||
68 | .on('end', () => res(tempFixturePath)) | ||
69 | .run() | ||
70 | }) | ||
71 | } | ||
72 | |||
73 | return tempFixturePath | ||
74 | } | ||
75 | |||
76 | export { | ||
77 | generateHighBitrateVideo, | ||
78 | generateVideoWithFramerate | ||
79 | } | ||
diff --git a/packages/tests/src/shared/live.ts b/packages/tests/src/shared/live.ts new file mode 100644 index 000000000..9c7991b0d --- /dev/null +++ b/packages/tests/src/shared/live.ts | |||
@@ -0,0 +1,186 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { readdir } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { sha1 } from '@peertube/peertube-node-utils' | ||
8 | import { LiveVideo, VideoStreamingPlaylistType } from '@peertube/peertube-models' | ||
9 | import { ObjectStorageCommand, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
10 | import { SQLCommand } from './sql-command.js' | ||
11 | import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist } from './streaming-playlists.js' | ||
12 | |||
13 | async function checkLiveCleanup (options: { | ||
14 | server: PeerTubeServer | ||
15 | videoUUID: string | ||
16 | permanent: boolean | ||
17 | savedResolutions?: number[] | ||
18 | }) { | ||
19 | const { server, videoUUID, permanent, savedResolutions = [] } = options | ||
20 | |||
21 | const basePath = server.servers.buildDirectory('streaming-playlists') | ||
22 | const hlsPath = join(basePath, 'hls', videoUUID) | ||
23 | |||
24 | if (permanent) { | ||
25 | if (!await pathExists(hlsPath)) return | ||
26 | |||
27 | const files = await readdir(hlsPath) | ||
28 | expect(files).to.have.lengthOf(0) | ||
29 | return | ||
30 | } | ||
31 | |||
32 | if (savedResolutions.length === 0) { | ||
33 | return checkUnsavedLiveCleanup(server, videoUUID, hlsPath) | ||
34 | } | ||
35 | |||
36 | return checkSavedLiveCleanup(hlsPath, savedResolutions) | ||
37 | } | ||
38 | |||
39 | // --------------------------------------------------------------------------- | ||
40 | |||
41 | async function testLiveVideoResolutions (options: { | ||
42 | sqlCommand: SQLCommand | ||
43 | originServer: PeerTubeServer | ||
44 | |||
45 | servers: PeerTubeServer[] | ||
46 | liveVideoId: string | ||
47 | resolutions: number[] | ||
48 | transcoded: boolean | ||
49 | |||
50 | objectStorage?: ObjectStorageCommand | ||
51 | objectStorageBaseUrl?: string | ||
52 | }) { | ||
53 | const { | ||
54 | originServer, | ||
55 | sqlCommand, | ||
56 | servers, | ||
57 | liveVideoId, | ||
58 | resolutions, | ||
59 | transcoded, | ||
60 | objectStorage, | ||
61 | objectStorageBaseUrl = objectStorage?.getMockPlaylistBaseUrl() | ||
62 | } = options | ||
63 | |||
64 | for (const server of servers) { | ||
65 | const { data } = await server.videos.list() | ||
66 | expect(data.find(v => v.uuid === liveVideoId)).to.exist | ||
67 | |||
68 | const video = await server.videos.get({ id: liveVideoId }) | ||
69 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
70 | |||
71 | const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS) | ||
72 | expect(hlsPlaylist).to.exist | ||
73 | expect(hlsPlaylist.files).to.have.lengthOf(0) // Only fragmented mp4 files are displayed | ||
74 | |||
75 | await checkResolutionsInMasterPlaylist({ | ||
76 | server, | ||
77 | playlistUrl: hlsPlaylist.playlistUrl, | ||
78 | resolutions, | ||
79 | transcoded, | ||
80 | withRetry: !!objectStorage | ||
81 | }) | ||
82 | |||
83 | if (objectStorage) { | ||
84 | expect(hlsPlaylist.playlistUrl).to.contain(objectStorageBaseUrl) | ||
85 | } | ||
86 | |||
87 | for (let i = 0; i < resolutions.length; i++) { | ||
88 | const segmentNum = 3 | ||
89 | const segmentName = `${i}-00000${segmentNum}.ts` | ||
90 | await originServer.live.waitUntilSegmentGeneration({ | ||
91 | server: originServer, | ||
92 | videoUUID: video.uuid, | ||
93 | playlistNumber: i, | ||
94 | segment: segmentNum, | ||
95 | objectStorage, | ||
96 | objectStorageBaseUrl | ||
97 | }) | ||
98 | |||
99 | const baseUrl = objectStorage | ||
100 | ? join(objectStorageBaseUrl, 'hls') | ||
101 | : originServer.url + '/static/streaming-playlists/hls' | ||
102 | |||
103 | if (objectStorage) { | ||
104 | expect(hlsPlaylist.segmentsSha256Url).to.contain(objectStorageBaseUrl) | ||
105 | } | ||
106 | |||
107 | const subPlaylist = await originServer.streamingPlaylists.get({ | ||
108 | url: `${baseUrl}/${video.uuid}/${i}.m3u8`, | ||
109 | withRetry: !!objectStorage // With object storage, the request may fail because of inconsistent data in S3 | ||
110 | }) | ||
111 | |||
112 | expect(subPlaylist).to.contain(segmentName) | ||
113 | |||
114 | await checkLiveSegmentHash({ | ||
115 | server, | ||
116 | baseUrlSegment: baseUrl, | ||
117 | videoUUID: video.uuid, | ||
118 | segmentName, | ||
119 | hlsPlaylist, | ||
120 | withRetry: !!objectStorage // With object storage, the request may fail because of inconsistent data in S3 | ||
121 | }) | ||
122 | |||
123 | if (originServer.internalServerNumber === server.internalServerNumber) { | ||
124 | const infohash = sha1(`${2 + hlsPlaylist.playlistUrl}+V${i}`) | ||
125 | const dbInfohashes = await sqlCommand.getPlaylistInfohash(hlsPlaylist.id) | ||
126 | |||
127 | expect(dbInfohashes).to.include(infohash) | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | |||
133 | // --------------------------------------------------------------------------- | ||
134 | |||
135 | export { | ||
136 | checkLiveCleanup, | ||
137 | testLiveVideoResolutions | ||
138 | } | ||
139 | |||
140 | // --------------------------------------------------------------------------- | ||
141 | |||
142 | async function checkSavedLiveCleanup (hlsPath: string, savedResolutions: number[] = []) { | ||
143 | const files = await readdir(hlsPath) | ||
144 | |||
145 | // fragmented file and playlist per resolution + master playlist + segments sha256 json file | ||
146 | expect(files, `Directory content: ${files.join(', ')}`).to.have.lengthOf(savedResolutions.length * 2 + 2) | ||
147 | |||
148 | for (const resolution of savedResolutions) { | ||
149 | const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`)) | ||
150 | expect(fragmentedFile).to.exist | ||
151 | |||
152 | const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`)) | ||
153 | expect(playlistFile).to.exist | ||
154 | } | ||
155 | |||
156 | const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8')) | ||
157 | expect(masterPlaylistFile).to.exist | ||
158 | |||
159 | const shaFile = files.find(f => f.endsWith('-segments-sha256.json')) | ||
160 | expect(shaFile).to.exist | ||
161 | } | ||
162 | |||
163 | async function checkUnsavedLiveCleanup (server: PeerTubeServer, videoUUID: string, hlsPath: string) { | ||
164 | let live: LiveVideo | ||
165 | |||
166 | try { | ||
167 | live = await server.live.get({ videoId: videoUUID }) | ||
168 | } catch {} | ||
169 | |||
170 | if (live?.permanentLive) { | ||
171 | expect(await pathExists(hlsPath)).to.be.true | ||
172 | |||
173 | const hlsFiles = await readdir(hlsPath) | ||
174 | expect(hlsFiles).to.have.lengthOf(1) // Only replays directory | ||
175 | |||
176 | const replayDir = join(hlsPath, 'replay') | ||
177 | expect(await pathExists(replayDir)).to.be.true | ||
178 | |||
179 | const replayFiles = await readdir(join(hlsPath, 'replay')) | ||
180 | expect(replayFiles).to.have.lengthOf(0) | ||
181 | |||
182 | return | ||
183 | } | ||
184 | |||
185 | expect(await pathExists(hlsPath)).to.be.false | ||
186 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/index.ts b/packages/tests/src/shared/mock-servers/index.ts new file mode 100644 index 000000000..9d1c63c67 --- /dev/null +++ b/packages/tests/src/shared/mock-servers/index.ts | |||
@@ -0,0 +1,8 @@ | |||
1 | export * from './mock-429.js' | ||
2 | export * from './mock-email.js' | ||
3 | export * from './mock-http.js' | ||
4 | export * from './mock-instances-index.js' | ||
5 | export * from './mock-joinpeertube-versions.js' | ||
6 | export * from './mock-object-storage.js' | ||
7 | export * from './mock-plugin-blocklist.js' | ||
8 | export * from './mock-proxy.js' | ||
diff --git a/packages/tests/src/shared/mock-servers/mock-429.ts b/packages/tests/src/shared/mock-servers/mock-429.ts new file mode 100644 index 000000000..5fcb1447d --- /dev/null +++ b/packages/tests/src/shared/mock-servers/mock-429.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import express from 'express' | ||
2 | import { Server } from 'http' | ||
3 | import { getPort, randomListen, terminateServer } from './shared.js' | ||
4 | |||
5 | export class Mock429 { | ||
6 | private server: Server | ||
7 | private responseSent = false | ||
8 | |||
9 | async initialize () { | ||
10 | const app = express() | ||
11 | |||
12 | app.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
13 | |||
14 | if (!this.responseSent) { | ||
15 | this.responseSent = true | ||
16 | |||
17 | // Retry after 5 seconds | ||
18 | res.header('retry-after', '2') | ||
19 | return res.sendStatus(429) | ||
20 | } | ||
21 | |||
22 | return res.sendStatus(200) | ||
23 | }) | ||
24 | |||
25 | this.server = await randomListen(app) | ||
26 | |||
27 | return getPort(this.server) | ||
28 | } | ||
29 | |||
30 | terminate () { | ||
31 | return terminateServer(this.server) | ||
32 | } | ||
33 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/mock-email.ts b/packages/tests/src/shared/mock-servers/mock-email.ts new file mode 100644 index 000000000..7c618e57f --- /dev/null +++ b/packages/tests/src/shared/mock-servers/mock-email.ts | |||
@@ -0,0 +1,62 @@ | |||
1 | import MailDev from '@peertube/maildev' | ||
2 | import { randomInt } from '@peertube/peertube-core-utils' | ||
3 | import { parallelTests } from '@peertube/peertube-node-utils' | ||
4 | |||
5 | class MockSmtpServer { | ||
6 | |||
7 | private static instance: MockSmtpServer | ||
8 | private started = false | ||
9 | private maildev: any | ||
10 | private emails: object[] | ||
11 | |||
12 | private constructor () { } | ||
13 | |||
14 | collectEmails (emailsCollection: object[]) { | ||
15 | return new Promise<number>((res, rej) => { | ||
16 | const port = parallelTests() ? randomInt(1025, 2000) : 1025 | ||
17 | this.emails = emailsCollection | ||
18 | |||
19 | if (this.started) { | ||
20 | return res(undefined) | ||
21 | } | ||
22 | |||
23 | this.maildev = new MailDev({ | ||
24 | ip: '127.0.0.1', | ||
25 | smtp: port, | ||
26 | disableWeb: true, | ||
27 | silent: true | ||
28 | }) | ||
29 | |||
30 | this.maildev.on('new', email => { | ||
31 | this.emails.push(email) | ||
32 | }) | ||
33 | |||
34 | this.maildev.listen(err => { | ||
35 | if (err) return rej(err) | ||
36 | |||
37 | this.started = true | ||
38 | |||
39 | return res(port) | ||
40 | }) | ||
41 | }) | ||
42 | } | ||
43 | |||
44 | kill () { | ||
45 | if (!this.maildev) return | ||
46 | |||
47 | this.maildev.close() | ||
48 | |||
49 | this.maildev = null | ||
50 | MockSmtpServer.instance = null | ||
51 | } | ||
52 | |||
53 | static get Instance () { | ||
54 | return this.instance || (this.instance = new this()) | ||
55 | } | ||
56 | } | ||
57 | |||
58 | // --------------------------------------------------------------------------- | ||
59 | |||
60 | export { | ||
61 | MockSmtpServer | ||
62 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/mock-http.ts b/packages/tests/src/shared/mock-servers/mock-http.ts new file mode 100644 index 000000000..bc1a9ce91 --- /dev/null +++ b/packages/tests/src/shared/mock-servers/mock-http.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | import express from 'express' | ||
2 | import { Server } from 'http' | ||
3 | import { getPort, randomListen, terminateServer } from './shared.js' | ||
4 | |||
5 | export class MockHTTP { | ||
6 | private server: Server | ||
7 | |||
8 | async initialize () { | ||
9 | const app = express() | ||
10 | |||
11 | app.get('/*', (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
12 | return res.sendStatus(200) | ||
13 | }) | ||
14 | |||
15 | this.server = await randomListen(app) | ||
16 | |||
17 | return getPort(this.server) | ||
18 | } | ||
19 | |||
20 | terminate () { | ||
21 | return terminateServer(this.server) | ||
22 | } | ||
23 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/mock-instances-index.ts b/packages/tests/src/shared/mock-servers/mock-instances-index.ts new file mode 100644 index 000000000..a21367358 --- /dev/null +++ b/packages/tests/src/shared/mock-servers/mock-instances-index.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import express from 'express' | ||
2 | import { Server } from 'http' | ||
3 | import { getPort, randomListen, terminateServer } from './shared.js' | ||
4 | |||
5 | export class MockInstancesIndex { | ||
6 | private server: Server | ||
7 | |||
8 | private readonly indexInstances: { host: string, createdAt: string }[] = [] | ||
9 | |||
10 | async initialize () { | ||
11 | const app = express() | ||
12 | |||
13 | app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
14 | if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url) | ||
15 | |||
16 | return next() | ||
17 | }) | ||
18 | |||
19 | app.get('/api/v1/instances/hosts', (req: express.Request, res: express.Response) => { | ||
20 | const since = req.query.since | ||
21 | |||
22 | const filtered = this.indexInstances.filter(i => { | ||
23 | if (!since) return true | ||
24 | |||
25 | return i.createdAt > since | ||
26 | }) | ||
27 | |||
28 | return res.json({ | ||
29 | total: filtered.length, | ||
30 | data: filtered | ||
31 | }) | ||
32 | }) | ||
33 | |||
34 | this.server = await randomListen(app) | ||
35 | |||
36 | return getPort(this.server) | ||
37 | } | ||
38 | |||
39 | addInstance (host: string) { | ||
40 | this.indexInstances.push({ host, createdAt: new Date().toISOString() }) | ||
41 | } | ||
42 | |||
43 | terminate () { | ||
44 | return terminateServer(this.server) | ||
45 | } | ||
46 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/mock-joinpeertube-versions.ts b/packages/tests/src/shared/mock-servers/mock-joinpeertube-versions.ts new file mode 100644 index 000000000..0783165e4 --- /dev/null +++ b/packages/tests/src/shared/mock-servers/mock-joinpeertube-versions.ts | |||
@@ -0,0 +1,34 @@ | |||
1 | import express from 'express' | ||
2 | import { Server } from 'http' | ||
3 | import { getPort, randomListen } from './shared.js' | ||
4 | |||
5 | export class MockJoinPeerTubeVersions { | ||
6 | private server: Server | ||
7 | private latestVersion: string | ||
8 | |||
9 | async initialize () { | ||
10 | const app = express() | ||
11 | |||
12 | app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
13 | if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url) | ||
14 | |||
15 | return next() | ||
16 | }) | ||
17 | |||
18 | app.get('/versions.json', (req: express.Request, res: express.Response) => { | ||
19 | return res.json({ | ||
20 | peertube: { | ||
21 | latestVersion: this.latestVersion | ||
22 | } | ||
23 | }) | ||
24 | }) | ||
25 | |||
26 | this.server = await randomListen(app) | ||
27 | |||
28 | return getPort(this.server) | ||
29 | } | ||
30 | |||
31 | setLatestVersion (latestVersion: string) { | ||
32 | this.latestVersion = latestVersion | ||
33 | } | ||
34 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/mock-object-storage.ts b/packages/tests/src/shared/mock-servers/mock-object-storage.ts new file mode 100644 index 000000000..f97c57fd7 --- /dev/null +++ b/packages/tests/src/shared/mock-servers/mock-object-storage.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | import express from 'express' | ||
2 | import got, { RequestError } from 'got' | ||
3 | import { Server } from 'http' | ||
4 | import { pipeline } from 'stream' | ||
5 | import { ObjectStorageCommand } from '@peertube/peertube-server-commands' | ||
6 | import { getPort, randomListen, terminateServer } from './shared.js' | ||
7 | |||
8 | export class MockObjectStorageProxy { | ||
9 | private server: Server | ||
10 | |||
11 | async initialize () { | ||
12 | const app = express() | ||
13 | |||
14 | app.get('/:bucketName/:path(*)', (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
15 | const url = `http://${req.params.bucketName}.${ObjectStorageCommand.getMockEndpointHost()}/${req.params.path}` | ||
16 | |||
17 | if (process.env.DEBUG) { | ||
18 | console.log('Receiving request on mocked server %s.', req.url) | ||
19 | console.log('Proxifying request to %s', url) | ||
20 | } | ||
21 | |||
22 | return pipeline( | ||
23 | got.stream(url, { throwHttpErrors: false }), | ||
24 | res, | ||
25 | (err: RequestError) => { | ||
26 | if (!err) return | ||
27 | |||
28 | console.error('Pipeline failed.', err) | ||
29 | } | ||
30 | ) | ||
31 | }) | ||
32 | |||
33 | this.server = await randomListen(app) | ||
34 | |||
35 | return getPort(this.server) | ||
36 | } | ||
37 | |||
38 | terminate () { | ||
39 | return terminateServer(this.server) | ||
40 | } | ||
41 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/mock-plugin-blocklist.ts b/packages/tests/src/shared/mock-servers/mock-plugin-blocklist.ts new file mode 100644 index 000000000..c0b6518ba --- /dev/null +++ b/packages/tests/src/shared/mock-servers/mock-plugin-blocklist.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import express, { Request, Response } from 'express' | ||
2 | import { Server } from 'http' | ||
3 | import { getPort, randomListen, terminateServer } from './shared.js' | ||
4 | |||
5 | type BlocklistResponse = { | ||
6 | data: { | ||
7 | value: string | ||
8 | action?: 'add' | 'remove' | ||
9 | updatedAt?: string | ||
10 | }[] | ||
11 | } | ||
12 | |||
13 | export class MockBlocklist { | ||
14 | private body: BlocklistResponse | ||
15 | private server: Server | ||
16 | |||
17 | async initialize () { | ||
18 | const app = express() | ||
19 | |||
20 | app.get('/blocklist', (req: Request, res: Response) => { | ||
21 | return res.json(this.body) | ||
22 | }) | ||
23 | |||
24 | this.server = await randomListen(app) | ||
25 | |||
26 | return getPort(this.server) | ||
27 | } | ||
28 | |||
29 | replace (body: BlocklistResponse) { | ||
30 | this.body = body | ||
31 | } | ||
32 | |||
33 | terminate () { | ||
34 | return terminateServer(this.server) | ||
35 | } | ||
36 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/mock-proxy.ts b/packages/tests/src/shared/mock-servers/mock-proxy.ts new file mode 100644 index 000000000..e731670d8 --- /dev/null +++ b/packages/tests/src/shared/mock-servers/mock-proxy.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { createServer, Server } from 'http' | ||
2 | import { createProxy } from 'proxy' | ||
3 | import { getPort, terminateServer } from './shared.js' | ||
4 | |||
5 | class MockProxy { | ||
6 | private server: Server | ||
7 | |||
8 | initialize () { | ||
9 | return new Promise<number>(res => { | ||
10 | this.server = createProxy(createServer()) | ||
11 | this.server.listen(0, () => res(getPort(this.server))) | ||
12 | }) | ||
13 | } | ||
14 | |||
15 | terminate () { | ||
16 | return terminateServer(this.server) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | MockProxy | ||
24 | } | ||
diff --git a/packages/tests/src/shared/mock-servers/shared.ts b/packages/tests/src/shared/mock-servers/shared.ts new file mode 100644 index 000000000..235642439 --- /dev/null +++ b/packages/tests/src/shared/mock-servers/shared.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import { Express } from 'express' | ||
2 | import { Server } from 'http' | ||
3 | import { AddressInfo } from 'net' | ||
4 | |||
5 | function randomListen (app: Express) { | ||
6 | return new Promise<Server>(res => { | ||
7 | const server = app.listen(0, () => res(server)) | ||
8 | }) | ||
9 | } | ||
10 | |||
11 | function getPort (server: Server) { | ||
12 | const address = server.address() as AddressInfo | ||
13 | |||
14 | return address.port | ||
15 | } | ||
16 | |||
17 | function terminateServer (server: Server) { | ||
18 | if (!server) return Promise.resolve() | ||
19 | |||
20 | return new Promise<void>((res, rej) => { | ||
21 | server.close(err => { | ||
22 | if (err) return rej(err) | ||
23 | |||
24 | return res() | ||
25 | }) | ||
26 | }) | ||
27 | } | ||
28 | |||
29 | export { | ||
30 | randomListen, | ||
31 | getPort, | ||
32 | terminateServer | ||
33 | } | ||
diff --git a/packages/tests/src/shared/notifications.ts b/packages/tests/src/shared/notifications.ts new file mode 100644 index 000000000..3accd7322 --- /dev/null +++ b/packages/tests/src/shared/notifications.ts | |||
@@ -0,0 +1,891 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { | ||
4 | AbuseState, | ||
5 | AbuseStateType, | ||
6 | PluginType_Type, | ||
7 | UserNotification, | ||
8 | UserNotificationSetting, | ||
9 | UserNotificationSettingValue, | ||
10 | UserNotificationType | ||
11 | } from '@peertube/peertube-models' | ||
12 | import { | ||
13 | ConfigCommand, | ||
14 | PeerTubeServer, | ||
15 | createMultipleServers, | ||
16 | doubleFollow, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultAccountAvatar, | ||
19 | setDefaultChannelAvatar, | ||
20 | setDefaultVideoChannel | ||
21 | } from '@peertube/peertube-server-commands' | ||
22 | import { expect } from 'chai' | ||
23 | import { inspect } from 'util' | ||
24 | import { MockSmtpServer } from './mock-servers/index.js' | ||
25 | |||
26 | type CheckerBaseParams = { | ||
27 | server: PeerTubeServer | ||
28 | emails: any[] | ||
29 | socketNotifications: UserNotification[] | ||
30 | token: string | ||
31 | check?: { web: boolean, mail: boolean } | ||
32 | } | ||
33 | |||
34 | type CheckerType = 'presence' | 'absence' | ||
35 | |||
36 | function getAllNotificationsSettings (): UserNotificationSetting { | ||
37 | return { | ||
38 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
39 | newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
40 | abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
41 | videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
42 | blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
43 | myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
44 | myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
45 | commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
46 | newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
47 | newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
48 | newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
49 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
50 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
51 | autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
52 | newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
53 | myVideoStudioEditionFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
54 | newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
55 | } | ||
56 | } | ||
57 | |||
58 | async function checkNewVideoFromSubscription (options: CheckerBaseParams & { | ||
59 | videoName: string | ||
60 | shortUUID: string | ||
61 | checkType: CheckerType | ||
62 | }) { | ||
63 | const { videoName, shortUUID } = options | ||
64 | const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION | ||
65 | |||
66 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
67 | if (checkType === 'presence') { | ||
68 | expect(notification).to.not.be.undefined | ||
69 | expect(notification.type).to.equal(notificationType) | ||
70 | |||
71 | checkVideo(notification.video, videoName, shortUUID) | ||
72 | checkActor(notification.video.channel) | ||
73 | } else { | ||
74 | expect(notification).to.satisfy((n: UserNotification) => { | ||
75 | return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName | ||
76 | }) | ||
77 | } | ||
78 | } | ||
79 | |||
80 | function emailNotificationFinder (email: object) { | ||
81 | const text = email['text'] | ||
82 | return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1 | ||
83 | } | ||
84 | |||
85 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
86 | } | ||
87 | |||
88 | async function checkVideoIsPublished (options: CheckerBaseParams & { | ||
89 | videoName: string | ||
90 | shortUUID: string | ||
91 | checkType: CheckerType | ||
92 | }) { | ||
93 | const { videoName, shortUUID } = options | ||
94 | const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED | ||
95 | |||
96 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
97 | if (checkType === 'presence') { | ||
98 | expect(notification).to.not.be.undefined | ||
99 | expect(notification.type).to.equal(notificationType) | ||
100 | |||
101 | checkVideo(notification.video, videoName, shortUUID) | ||
102 | checkActor(notification.video.channel) | ||
103 | } else { | ||
104 | expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) | ||
105 | } | ||
106 | } | ||
107 | |||
108 | function emailNotificationFinder (email: object) { | ||
109 | const text: string = email['text'] | ||
110 | return text.includes(shortUUID) && text.includes('Your video') | ||
111 | } | ||
112 | |||
113 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
114 | } | ||
115 | |||
116 | async function checkVideoStudioEditionIsFinished (options: CheckerBaseParams & { | ||
117 | videoName: string | ||
118 | shortUUID: string | ||
119 | checkType: CheckerType | ||
120 | }) { | ||
121 | const { videoName, shortUUID } = options | ||
122 | const notificationType = UserNotificationType.MY_VIDEO_STUDIO_EDITION_FINISHED | ||
123 | |||
124 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
125 | if (checkType === 'presence') { | ||
126 | expect(notification).to.not.be.undefined | ||
127 | expect(notification.type).to.equal(notificationType) | ||
128 | |||
129 | checkVideo(notification.video, videoName, shortUUID) | ||
130 | checkActor(notification.video.channel) | ||
131 | } else { | ||
132 | expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) | ||
133 | } | ||
134 | } | ||
135 | |||
136 | function emailNotificationFinder (email: object) { | ||
137 | const text: string = email['text'] | ||
138 | return text.includes(shortUUID) && text.includes('Edition of your video') | ||
139 | } | ||
140 | |||
141 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
142 | } | ||
143 | |||
144 | async function checkMyVideoImportIsFinished (options: CheckerBaseParams & { | ||
145 | videoName: string | ||
146 | shortUUID: string | ||
147 | url: string | ||
148 | success: boolean | ||
149 | checkType: CheckerType | ||
150 | }) { | ||
151 | const { videoName, shortUUID, url, success } = options | ||
152 | |||
153 | const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR | ||
154 | |||
155 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
156 | if (checkType === 'presence') { | ||
157 | expect(notification).to.not.be.undefined | ||
158 | expect(notification.type).to.equal(notificationType) | ||
159 | |||
160 | expect(notification.videoImport.targetUrl).to.equal(url) | ||
161 | |||
162 | if (success) checkVideo(notification.videoImport.video, videoName, shortUUID) | ||
163 | } else { | ||
164 | expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url) | ||
165 | } | ||
166 | } | ||
167 | |||
168 | function emailNotificationFinder (email: object) { | ||
169 | const text: string = email['text'] | ||
170 | const toFind = success ? ' finished' : ' error' | ||
171 | |||
172 | return text.includes(url) && text.includes(toFind) | ||
173 | } | ||
174 | |||
175 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
176 | } | ||
177 | |||
178 | // --------------------------------------------------------------------------- | ||
179 | |||
180 | async function checkUserRegistered (options: CheckerBaseParams & { | ||
181 | username: string | ||
182 | checkType: CheckerType | ||
183 | }) { | ||
184 | const { username } = options | ||
185 | const notificationType = UserNotificationType.NEW_USER_REGISTRATION | ||
186 | |||
187 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
188 | if (checkType === 'presence') { | ||
189 | expect(notification).to.not.be.undefined | ||
190 | expect(notification.type).to.equal(notificationType) | ||
191 | |||
192 | checkActor(notification.account, { withAvatar: false }) | ||
193 | expect(notification.account.name).to.equal(username) | ||
194 | } else { | ||
195 | expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username) | ||
196 | } | ||
197 | } | ||
198 | |||
199 | function emailNotificationFinder (email: object) { | ||
200 | const text: string = email['text'] | ||
201 | |||
202 | return text.includes(' registered.') && text.includes(username) | ||
203 | } | ||
204 | |||
205 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
206 | } | ||
207 | |||
208 | async function checkRegistrationRequest (options: CheckerBaseParams & { | ||
209 | username: string | ||
210 | registrationReason: string | ||
211 | checkType: CheckerType | ||
212 | }) { | ||
213 | const { username, registrationReason } = options | ||
214 | const notificationType = UserNotificationType.NEW_USER_REGISTRATION_REQUEST | ||
215 | |||
216 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
217 | if (checkType === 'presence') { | ||
218 | expect(notification).to.not.be.undefined | ||
219 | expect(notification.type).to.equal(notificationType) | ||
220 | |||
221 | expect(notification.registration.username).to.equal(username) | ||
222 | } else { | ||
223 | expect(notification).to.satisfy(n => n.type !== notificationType || n.registration.username !== username) | ||
224 | } | ||
225 | } | ||
226 | |||
227 | function emailNotificationFinder (email: object) { | ||
228 | const text: string = email['text'] | ||
229 | |||
230 | return text.includes(' wants to register ') && text.includes(username) && text.includes(registrationReason) | ||
231 | } | ||
232 | |||
233 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
234 | } | ||
235 | |||
236 | // --------------------------------------------------------------------------- | ||
237 | |||
238 | async function checkNewActorFollow (options: CheckerBaseParams & { | ||
239 | followType: 'channel' | 'account' | ||
240 | followerName: string | ||
241 | followerDisplayName: string | ||
242 | followingDisplayName: string | ||
243 | checkType: CheckerType | ||
244 | }) { | ||
245 | const { followType, followerName, followerDisplayName, followingDisplayName } = options | ||
246 | const notificationType = UserNotificationType.NEW_FOLLOW | ||
247 | |||
248 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
249 | if (checkType === 'presence') { | ||
250 | expect(notification).to.not.be.undefined | ||
251 | expect(notification.type).to.equal(notificationType) | ||
252 | |||
253 | checkActor(notification.actorFollow.follower) | ||
254 | expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName) | ||
255 | expect(notification.actorFollow.follower.name).to.equal(followerName) | ||
256 | expect(notification.actorFollow.follower.host).to.not.be.undefined | ||
257 | |||
258 | const following = notification.actorFollow.following | ||
259 | expect(following.displayName).to.equal(followingDisplayName) | ||
260 | expect(following.type).to.equal(followType) | ||
261 | } else { | ||
262 | expect(notification).to.satisfy(n => { | ||
263 | return n.type !== notificationType || | ||
264 | (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName) | ||
265 | }) | ||
266 | } | ||
267 | } | ||
268 | |||
269 | function emailNotificationFinder (email: object) { | ||
270 | const text: string = email['text'] | ||
271 | |||
272 | return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName) | ||
273 | } | ||
274 | |||
275 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
276 | } | ||
277 | |||
278 | async function checkNewInstanceFollower (options: CheckerBaseParams & { | ||
279 | followerHost: string | ||
280 | checkType: CheckerType | ||
281 | }) { | ||
282 | const { followerHost } = options | ||
283 | const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER | ||
284 | |||
285 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
286 | if (checkType === 'presence') { | ||
287 | expect(notification).to.not.be.undefined | ||
288 | expect(notification.type).to.equal(notificationType) | ||
289 | |||
290 | checkActor(notification.actorFollow.follower, { withAvatar: false }) | ||
291 | expect(notification.actorFollow.follower.name).to.equal('peertube') | ||
292 | expect(notification.actorFollow.follower.host).to.equal(followerHost) | ||
293 | |||
294 | expect(notification.actorFollow.following.name).to.equal('peertube') | ||
295 | } else { | ||
296 | expect(notification).to.satisfy(n => { | ||
297 | return n.type !== notificationType || n.actorFollow.follower.host !== followerHost | ||
298 | }) | ||
299 | } | ||
300 | } | ||
301 | |||
302 | function emailNotificationFinder (email: object) { | ||
303 | const text: string = email['text'] | ||
304 | |||
305 | return text.includes('instance has a new follower') && text.includes(followerHost) | ||
306 | } | ||
307 | |||
308 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
309 | } | ||
310 | |||
311 | async function checkAutoInstanceFollowing (options: CheckerBaseParams & { | ||
312 | followerHost: string | ||
313 | followingHost: string | ||
314 | checkType: CheckerType | ||
315 | }) { | ||
316 | const { followerHost, followingHost } = options | ||
317 | const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING | ||
318 | |||
319 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
320 | if (checkType === 'presence') { | ||
321 | expect(notification).to.not.be.undefined | ||
322 | expect(notification.type).to.equal(notificationType) | ||
323 | |||
324 | const following = notification.actorFollow.following | ||
325 | |||
326 | checkActor(following, { withAvatar: false }) | ||
327 | expect(following.name).to.equal('peertube') | ||
328 | expect(following.host).to.equal(followingHost) | ||
329 | |||
330 | expect(notification.actorFollow.follower.name).to.equal('peertube') | ||
331 | expect(notification.actorFollow.follower.host).to.equal(followerHost) | ||
332 | } else { | ||
333 | expect(notification).to.satisfy(n => { | ||
334 | return n.type !== notificationType || n.actorFollow.following.host !== followingHost | ||
335 | }) | ||
336 | } | ||
337 | } | ||
338 | |||
339 | function emailNotificationFinder (email: object) { | ||
340 | const text: string = email['text'] | ||
341 | |||
342 | return text.includes(' automatically followed a new instance') && text.includes(followingHost) | ||
343 | } | ||
344 | |||
345 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
346 | } | ||
347 | |||
348 | async function checkCommentMention (options: CheckerBaseParams & { | ||
349 | shortUUID: string | ||
350 | commentId: number | ||
351 | threadId: number | ||
352 | byAccountDisplayName: string | ||
353 | checkType: CheckerType | ||
354 | }) { | ||
355 | const { shortUUID, commentId, threadId, byAccountDisplayName } = options | ||
356 | const notificationType = UserNotificationType.COMMENT_MENTION | ||
357 | |||
358 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
359 | if (checkType === 'presence') { | ||
360 | expect(notification).to.not.be.undefined | ||
361 | expect(notification.type).to.equal(notificationType) | ||
362 | |||
363 | checkComment(notification.comment, commentId, threadId) | ||
364 | checkActor(notification.comment.account) | ||
365 | expect(notification.comment.account.displayName).to.equal(byAccountDisplayName) | ||
366 | |||
367 | checkVideo(notification.comment.video, undefined, shortUUID) | ||
368 | } else { | ||
369 | expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId) | ||
370 | } | ||
371 | } | ||
372 | |||
373 | function emailNotificationFinder (email: object) { | ||
374 | const text: string = email['text'] | ||
375 | |||
376 | return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName) | ||
377 | } | ||
378 | |||
379 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
380 | } | ||
381 | |||
382 | let lastEmailCount = 0 | ||
383 | |||
384 | async function checkNewCommentOnMyVideo (options: CheckerBaseParams & { | ||
385 | shortUUID: string | ||
386 | commentId: number | ||
387 | threadId: number | ||
388 | checkType: CheckerType | ||
389 | }) { | ||
390 | const { server, shortUUID, commentId, threadId, checkType, emails } = options | ||
391 | const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO | ||
392 | |||
393 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
394 | if (checkType === 'presence') { | ||
395 | expect(notification).to.not.be.undefined | ||
396 | expect(notification.type).to.equal(notificationType) | ||
397 | |||
398 | checkComment(notification.comment, commentId, threadId) | ||
399 | checkActor(notification.comment.account) | ||
400 | checkVideo(notification.comment.video, undefined, shortUUID) | ||
401 | } else { | ||
402 | expect(notification).to.satisfy((n: UserNotification) => { | ||
403 | return n === undefined || n.comment === undefined || n.comment.id !== commentId | ||
404 | }) | ||
405 | } | ||
406 | } | ||
407 | |||
408 | const commentUrl = `${server.url}/w/${shortUUID};threadId=${threadId}` | ||
409 | |||
410 | function emailNotificationFinder (email: object) { | ||
411 | return email['text'].indexOf(commentUrl) !== -1 | ||
412 | } | ||
413 | |||
414 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
415 | |||
416 | if (checkType === 'presence') { | ||
417 | // We cannot detect email duplicates, so check we received another email | ||
418 | expect(emails).to.have.length.above(lastEmailCount) | ||
419 | lastEmailCount = emails.length | ||
420 | } | ||
421 | } | ||
422 | |||
423 | async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & { | ||
424 | shortUUID: string | ||
425 | videoName: string | ||
426 | checkType: CheckerType | ||
427 | }) { | ||
428 | const { shortUUID, videoName } = options | ||
429 | const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS | ||
430 | |||
431 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
432 | if (checkType === 'presence') { | ||
433 | expect(notification).to.not.be.undefined | ||
434 | expect(notification.type).to.equal(notificationType) | ||
435 | |||
436 | expect(notification.abuse.id).to.be.a('number') | ||
437 | checkVideo(notification.abuse.video, videoName, shortUUID) | ||
438 | } else { | ||
439 | expect(notification).to.satisfy((n: UserNotification) => { | ||
440 | return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID | ||
441 | }) | ||
442 | } | ||
443 | } | ||
444 | |||
445 | function emailNotificationFinder (email: object) { | ||
446 | const text = email['text'] | ||
447 | return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1 | ||
448 | } | ||
449 | |||
450 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
451 | } | ||
452 | |||
453 | async function checkNewAbuseMessage (options: CheckerBaseParams & { | ||
454 | abuseId: number | ||
455 | message: string | ||
456 | toEmail: string | ||
457 | checkType: CheckerType | ||
458 | }) { | ||
459 | const { abuseId, message, toEmail } = options | ||
460 | const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE | ||
461 | |||
462 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
463 | if (checkType === 'presence') { | ||
464 | expect(notification).to.not.be.undefined | ||
465 | expect(notification.type).to.equal(notificationType) | ||
466 | |||
467 | expect(notification.abuse.id).to.equal(abuseId) | ||
468 | } else { | ||
469 | expect(notification).to.satisfy((n: UserNotification) => { | ||
470 | return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId | ||
471 | }) | ||
472 | } | ||
473 | } | ||
474 | |||
475 | function emailNotificationFinder (email: object) { | ||
476 | const text = email['text'] | ||
477 | const to = email['to'].filter(t => t.address === toEmail) | ||
478 | |||
479 | return text.indexOf(message) !== -1 && to.length !== 0 | ||
480 | } | ||
481 | |||
482 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
483 | } | ||
484 | |||
485 | async function checkAbuseStateChange (options: CheckerBaseParams & { | ||
486 | abuseId: number | ||
487 | state: AbuseStateType | ||
488 | checkType: CheckerType | ||
489 | }) { | ||
490 | const { abuseId, state } = options | ||
491 | const notificationType = UserNotificationType.ABUSE_STATE_CHANGE | ||
492 | |||
493 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
494 | if (checkType === 'presence') { | ||
495 | expect(notification).to.not.be.undefined | ||
496 | expect(notification.type).to.equal(notificationType) | ||
497 | |||
498 | expect(notification.abuse.id).to.equal(abuseId) | ||
499 | expect(notification.abuse.state).to.equal(state) | ||
500 | } else { | ||
501 | expect(notification).to.satisfy((n: UserNotification) => { | ||
502 | return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId | ||
503 | }) | ||
504 | } | ||
505 | } | ||
506 | |||
507 | function emailNotificationFinder (email: object) { | ||
508 | const text = email['text'] | ||
509 | |||
510 | const contains = state === AbuseState.ACCEPTED | ||
511 | ? ' accepted' | ||
512 | : ' rejected' | ||
513 | |||
514 | return text.indexOf(contains) !== -1 | ||
515 | } | ||
516 | |||
517 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
518 | } | ||
519 | |||
520 | async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & { | ||
521 | shortUUID: string | ||
522 | videoName: string | ||
523 | checkType: CheckerType | ||
524 | }) { | ||
525 | const { shortUUID, videoName } = options | ||
526 | const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS | ||
527 | |||
528 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
529 | if (checkType === 'presence') { | ||
530 | expect(notification).to.not.be.undefined | ||
531 | expect(notification.type).to.equal(notificationType) | ||
532 | |||
533 | expect(notification.abuse.id).to.be.a('number') | ||
534 | checkVideo(notification.abuse.comment.video, videoName, shortUUID) | ||
535 | } else { | ||
536 | expect(notification).to.satisfy((n: UserNotification) => { | ||
537 | return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID | ||
538 | }) | ||
539 | } | ||
540 | } | ||
541 | |||
542 | function emailNotificationFinder (email: object) { | ||
543 | const text = email['text'] | ||
544 | return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1 | ||
545 | } | ||
546 | |||
547 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
548 | } | ||
549 | |||
550 | async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & { | ||
551 | displayName: string | ||
552 | checkType: CheckerType | ||
553 | }) { | ||
554 | const { displayName } = options | ||
555 | const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS | ||
556 | |||
557 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
558 | if (checkType === 'presence') { | ||
559 | expect(notification).to.not.be.undefined | ||
560 | expect(notification.type).to.equal(notificationType) | ||
561 | |||
562 | expect(notification.abuse.id).to.be.a('number') | ||
563 | expect(notification.abuse.account.displayName).to.equal(displayName) | ||
564 | } else { | ||
565 | expect(notification).to.satisfy((n: UserNotification) => { | ||
566 | return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName | ||
567 | }) | ||
568 | } | ||
569 | } | ||
570 | |||
571 | function emailNotificationFinder (email: object) { | ||
572 | const text = email['text'] | ||
573 | return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1 | ||
574 | } | ||
575 | |||
576 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
577 | } | ||
578 | |||
579 | async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & { | ||
580 | shortUUID: string | ||
581 | videoName: string | ||
582 | checkType: CheckerType | ||
583 | }) { | ||
584 | const { shortUUID, videoName } = options | ||
585 | const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS | ||
586 | |||
587 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
588 | if (checkType === 'presence') { | ||
589 | expect(notification).to.not.be.undefined | ||
590 | expect(notification.type).to.equal(notificationType) | ||
591 | |||
592 | expect(notification.videoBlacklist.video.id).to.be.a('number') | ||
593 | checkVideo(notification.videoBlacklist.video, videoName, shortUUID) | ||
594 | } else { | ||
595 | expect(notification).to.satisfy((n: UserNotification) => { | ||
596 | return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID | ||
597 | }) | ||
598 | } | ||
599 | } | ||
600 | |||
601 | function emailNotificationFinder (email: object) { | ||
602 | const text = email['text'] | ||
603 | return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1 | ||
604 | } | ||
605 | |||
606 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
607 | } | ||
608 | |||
609 | async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & { | ||
610 | shortUUID: string | ||
611 | videoName: string | ||
612 | blacklistType: 'blacklist' | 'unblacklist' | ||
613 | }) { | ||
614 | const { videoName, shortUUID, blacklistType } = options | ||
615 | const notificationType = blacklistType === 'blacklist' | ||
616 | ? UserNotificationType.BLACKLIST_ON_MY_VIDEO | ||
617 | : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO | ||
618 | |||
619 | function notificationChecker (notification: UserNotification) { | ||
620 | expect(notification).to.not.be.undefined | ||
621 | expect(notification.type).to.equal(notificationType) | ||
622 | |||
623 | const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video | ||
624 | |||
625 | checkVideo(video, videoName, shortUUID) | ||
626 | } | ||
627 | |||
628 | function emailNotificationFinder (email: object) { | ||
629 | const text = email['text'] | ||
630 | const blacklistText = blacklistType === 'blacklist' | ||
631 | ? 'blacklisted' | ||
632 | : 'unblacklisted' | ||
633 | |||
634 | return text.includes(shortUUID) && text.includes(blacklistText) | ||
635 | } | ||
636 | |||
637 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' }) | ||
638 | } | ||
639 | |||
640 | async function checkNewPeerTubeVersion (options: CheckerBaseParams & { | ||
641 | latestVersion: string | ||
642 | checkType: CheckerType | ||
643 | }) { | ||
644 | const { latestVersion } = options | ||
645 | const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION | ||
646 | |||
647 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
648 | if (checkType === 'presence') { | ||
649 | expect(notification).to.not.be.undefined | ||
650 | expect(notification.type).to.equal(notificationType) | ||
651 | |||
652 | expect(notification.peertube).to.exist | ||
653 | expect(notification.peertube.latestVersion).to.equal(latestVersion) | ||
654 | } else { | ||
655 | expect(notification).to.satisfy((n: UserNotification) => { | ||
656 | return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion | ||
657 | }) | ||
658 | } | ||
659 | } | ||
660 | |||
661 | function emailNotificationFinder (email: object) { | ||
662 | const text = email['text'] | ||
663 | |||
664 | return text.includes(latestVersion) | ||
665 | } | ||
666 | |||
667 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
668 | } | ||
669 | |||
670 | async function checkNewPluginVersion (options: CheckerBaseParams & { | ||
671 | pluginType: PluginType_Type | ||
672 | pluginName: string | ||
673 | checkType: CheckerType | ||
674 | }) { | ||
675 | const { pluginName, pluginType } = options | ||
676 | const notificationType = UserNotificationType.NEW_PLUGIN_VERSION | ||
677 | |||
678 | function notificationChecker (notification: UserNotification, checkType: CheckerType) { | ||
679 | if (checkType === 'presence') { | ||
680 | expect(notification).to.not.be.undefined | ||
681 | expect(notification.type).to.equal(notificationType) | ||
682 | |||
683 | expect(notification.plugin.name).to.equal(pluginName) | ||
684 | expect(notification.plugin.type).to.equal(pluginType) | ||
685 | } else { | ||
686 | expect(notification).to.satisfy((n: UserNotification) => { | ||
687 | return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName | ||
688 | }) | ||
689 | } | ||
690 | } | ||
691 | |||
692 | function emailNotificationFinder (email: object) { | ||
693 | const text = email['text'] | ||
694 | |||
695 | return text.includes(pluginName) | ||
696 | } | ||
697 | |||
698 | await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) | ||
699 | } | ||
700 | |||
701 | async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) { | ||
702 | const userNotifications: UserNotification[] = [] | ||
703 | const adminNotifications: UserNotification[] = [] | ||
704 | const adminNotificationsServer2: UserNotification[] = [] | ||
705 | const emails: object[] = [] | ||
706 | |||
707 | const port = await MockSmtpServer.Instance.collectEmails(emails) | ||
708 | |||
709 | const overrideConfig = { | ||
710 | ...ConfigCommand.getEmailOverrideConfig(port), | ||
711 | |||
712 | signup: { | ||
713 | limit: 20 | ||
714 | } | ||
715 | } | ||
716 | const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) | ||
717 | |||
718 | await setAccessTokensToServers(servers) | ||
719 | await setDefaultVideoChannel(servers) | ||
720 | await setDefaultChannelAvatar(servers) | ||
721 | await setDefaultAccountAvatar(servers) | ||
722 | |||
723 | if (servers[1]) { | ||
724 | await servers[1].config.enableStudio() | ||
725 | await servers[1].config.enableLive({ allowReplay: true, transcoding: false }) | ||
726 | } | ||
727 | |||
728 | if (serversCount > 1) { | ||
729 | await doubleFollow(servers[0], servers[1]) | ||
730 | } | ||
731 | |||
732 | const user = { username: 'user_1', password: 'super password' } | ||
733 | await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 }) | ||
734 | const userAccessToken = await servers[0].login.getAccessToken(user) | ||
735 | |||
736 | await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() }) | ||
737 | await servers[0].users.updateMyAvatar({ token: userAccessToken, fixture: 'avatar.png' }) | ||
738 | await servers[0].channels.updateImage({ channelName: 'user_1_channel', token: userAccessToken, fixture: 'avatar.png', type: 'avatar' }) | ||
739 | |||
740 | await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() }) | ||
741 | |||
742 | if (serversCount > 1) { | ||
743 | await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() }) | ||
744 | } | ||
745 | |||
746 | { | ||
747 | const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken }) | ||
748 | socket.on('new-notification', n => userNotifications.push(n)) | ||
749 | } | ||
750 | { | ||
751 | const socket = servers[0].socketIO.getUserNotificationSocket() | ||
752 | socket.on('new-notification', n => adminNotifications.push(n)) | ||
753 | } | ||
754 | |||
755 | if (serversCount > 1) { | ||
756 | const socket = servers[1].socketIO.getUserNotificationSocket() | ||
757 | socket.on('new-notification', n => adminNotificationsServer2.push(n)) | ||
758 | } | ||
759 | |||
760 | const { videoChannels } = await servers[0].users.getMyInfo() | ||
761 | const channelId = videoChannels[0].id | ||
762 | |||
763 | return { | ||
764 | userNotifications, | ||
765 | adminNotifications, | ||
766 | adminNotificationsServer2, | ||
767 | userAccessToken, | ||
768 | emails, | ||
769 | servers, | ||
770 | channelId, | ||
771 | baseOverrideConfig: overrideConfig | ||
772 | } | ||
773 | } | ||
774 | |||
775 | // --------------------------------------------------------------------------- | ||
776 | |||
777 | export { | ||
778 | type CheckerType, | ||
779 | type CheckerBaseParams, | ||
780 | |||
781 | getAllNotificationsSettings, | ||
782 | |||
783 | checkMyVideoImportIsFinished, | ||
784 | checkUserRegistered, | ||
785 | checkAutoInstanceFollowing, | ||
786 | checkVideoIsPublished, | ||
787 | checkNewVideoFromSubscription, | ||
788 | checkNewActorFollow, | ||
789 | checkNewCommentOnMyVideo, | ||
790 | checkNewBlacklistOnMyVideo, | ||
791 | checkCommentMention, | ||
792 | checkNewVideoAbuseForModerators, | ||
793 | checkVideoAutoBlacklistForModerators, | ||
794 | checkNewAbuseMessage, | ||
795 | checkAbuseStateChange, | ||
796 | checkNewInstanceFollower, | ||
797 | prepareNotificationsTest, | ||
798 | checkNewCommentAbuseForModerators, | ||
799 | checkNewAccountAbuseForModerators, | ||
800 | checkNewPeerTubeVersion, | ||
801 | checkNewPluginVersion, | ||
802 | checkVideoStudioEditionIsFinished, | ||
803 | checkRegistrationRequest | ||
804 | } | ||
805 | |||
806 | // --------------------------------------------------------------------------- | ||
807 | |||
808 | async function checkNotification (options: CheckerBaseParams & { | ||
809 | notificationChecker: (notification: UserNotification, checkType: CheckerType) => void | ||
810 | emailNotificationFinder: (email: object) => boolean | ||
811 | checkType: CheckerType | ||
812 | }) { | ||
813 | const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options | ||
814 | |||
815 | const check = options.check || { web: true, mail: true } | ||
816 | |||
817 | if (check.web) { | ||
818 | const notification = await server.notifications.getLatest({ token }) | ||
819 | |||
820 | if (notification || checkType !== 'absence') { | ||
821 | notificationChecker(notification, checkType) | ||
822 | } | ||
823 | |||
824 | const socketNotification = socketNotifications.find(n => { | ||
825 | try { | ||
826 | notificationChecker(n, 'presence') | ||
827 | return true | ||
828 | } catch { | ||
829 | return false | ||
830 | } | ||
831 | }) | ||
832 | |||
833 | if (checkType === 'presence') { | ||
834 | const obj = inspect(socketNotifications, { depth: 5 }) | ||
835 | expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined | ||
836 | } else { | ||
837 | const obj = inspect(socketNotification, { depth: 5 }) | ||
838 | expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined | ||
839 | } | ||
840 | } | ||
841 | |||
842 | if (check.mail) { | ||
843 | // Last email | ||
844 | const email = emails | ||
845 | .slice() | ||
846 | .reverse() | ||
847 | .find(e => emailNotificationFinder(e)) | ||
848 | |||
849 | if (checkType === 'presence') { | ||
850 | const texts = emails.map(e => e.text) | ||
851 | expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined | ||
852 | } else { | ||
853 | expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined | ||
854 | } | ||
855 | } | ||
856 | } | ||
857 | |||
858 | function checkVideo (video: any, videoName?: string, shortUUID?: string) { | ||
859 | if (videoName) { | ||
860 | expect(video.name).to.be.a('string') | ||
861 | expect(video.name).to.not.be.empty | ||
862 | expect(video.name).to.equal(videoName) | ||
863 | } | ||
864 | |||
865 | if (shortUUID) { | ||
866 | expect(video.shortUUID).to.be.a('string') | ||
867 | expect(video.shortUUID).to.not.be.empty | ||
868 | expect(video.shortUUID).to.equal(shortUUID) | ||
869 | } | ||
870 | |||
871 | expect(video.id).to.be.a('number') | ||
872 | } | ||
873 | |||
874 | function checkActor (actor: any, options: { withAvatar?: boolean } = {}) { | ||
875 | const { withAvatar = true } = options | ||
876 | |||
877 | expect(actor.displayName).to.be.a('string') | ||
878 | expect(actor.displayName).to.not.be.empty | ||
879 | expect(actor.host).to.not.be.undefined | ||
880 | |||
881 | if (withAvatar) { | ||
882 | expect(actor.avatars).to.be.an('array') | ||
883 | expect(actor.avatars).to.have.lengthOf(2) | ||
884 | expect(actor.avatars[0].path).to.exist.and.not.empty | ||
885 | } | ||
886 | } | ||
887 | |||
888 | function checkComment (comment: any, commentId: number, threadId: number) { | ||
889 | expect(comment.id).to.equal(commentId) | ||
890 | expect(comment.threadId).to.equal(threadId) | ||
891 | } | ||
diff --git a/packages/tests/src/shared/peertube-runner-process.ts b/packages/tests/src/shared/peertube-runner-process.ts new file mode 100644 index 000000000..3d1f299f2 --- /dev/null +++ b/packages/tests/src/shared/peertube-runner-process.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | import { ChildProcess, fork, ForkOptions } from 'child_process' | ||
2 | import execa from 'execa' | ||
3 | import { join } from 'path' | ||
4 | import { root } from '@peertube/peertube-node-utils' | ||
5 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
6 | |||
7 | export class PeerTubeRunnerProcess { | ||
8 | private app?: ChildProcess | ||
9 | |||
10 | constructor (private readonly server: PeerTubeServer) { | ||
11 | |||
12 | } | ||
13 | |||
14 | runServer (options: { | ||
15 | hideLogs?: boolean // default true | ||
16 | } = {}) { | ||
17 | const { hideLogs = true } = options | ||
18 | |||
19 | return new Promise<void>((res, rej) => { | ||
20 | const args = [ 'server', '--verbose', ...this.buildIdArg() ] | ||
21 | |||
22 | const forkOptions: ForkOptions = { | ||
23 | detached: false, | ||
24 | silent: true, | ||
25 | execArgv: [] // Don't inject parent node options | ||
26 | } | ||
27 | |||
28 | this.app = fork(this.getRunnerPath(), args, forkOptions) | ||
29 | |||
30 | this.app.stdout.on('data', data => { | ||
31 | const str = data.toString() as string | ||
32 | |||
33 | if (!hideLogs) { | ||
34 | console.log(str) | ||
35 | } | ||
36 | }) | ||
37 | |||
38 | res() | ||
39 | }) | ||
40 | } | ||
41 | |||
42 | registerPeerTubeInstance (options: { | ||
43 | registrationToken: string | ||
44 | runnerName: string | ||
45 | runnerDescription?: string | ||
46 | }) { | ||
47 | const { registrationToken, runnerName, runnerDescription } = options | ||
48 | |||
49 | const args = [ | ||
50 | 'register', | ||
51 | '--url', this.server.url, | ||
52 | '--registration-token', registrationToken, | ||
53 | '--runner-name', runnerName, | ||
54 | ...this.buildIdArg() | ||
55 | ] | ||
56 | |||
57 | if (runnerDescription) { | ||
58 | args.push('--runner-description') | ||
59 | args.push(runnerDescription) | ||
60 | } | ||
61 | |||
62 | return this.runCommand(this.getRunnerPath(), args) | ||
63 | } | ||
64 | |||
65 | unregisterPeerTubeInstance (options: { | ||
66 | runnerName: string | ||
67 | }) { | ||
68 | const { runnerName } = options | ||
69 | |||
70 | const args = [ 'unregister', '--url', this.server.url, '--runner-name', runnerName, ...this.buildIdArg() ] | ||
71 | return this.runCommand(this.getRunnerPath(), args) | ||
72 | } | ||
73 | |||
74 | async listRegisteredPeerTubeInstances () { | ||
75 | const args = [ 'list-registered', ...this.buildIdArg() ] | ||
76 | const { stdout } = await this.runCommand(this.getRunnerPath(), args) | ||
77 | |||
78 | return stdout | ||
79 | } | ||
80 | |||
81 | kill () { | ||
82 | if (!this.app) return | ||
83 | |||
84 | process.kill(this.app.pid) | ||
85 | |||
86 | this.app = null | ||
87 | } | ||
88 | |||
89 | getId () { | ||
90 | return 'test-' + this.server.internalServerNumber | ||
91 | } | ||
92 | |||
93 | private getRunnerPath () { | ||
94 | return join(root(), 'apps', 'peertube-runner', 'dist', 'peertube-runner.js') | ||
95 | } | ||
96 | |||
97 | private buildIdArg () { | ||
98 | return [ '--id', this.getId() ] | ||
99 | } | ||
100 | |||
101 | private runCommand (path: string, args: string[]) { | ||
102 | return execa.node(path, args, { env: { ...process.env, NODE_OPTIONS: '' } }) | ||
103 | } | ||
104 | } | ||
diff --git a/packages/tests/src/shared/plugins.ts b/packages/tests/src/shared/plugins.ts new file mode 100644 index 000000000..c2afcbcbf --- /dev/null +++ b/packages/tests/src/shared/plugins.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
5 | |||
6 | async function testHelloWorldRegisteredSettings (server: PeerTubeServer) { | ||
7 | const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' }) | ||
8 | |||
9 | const registeredSettings = body.registeredSettings | ||
10 | expect(registeredSettings).to.have.length.at.least(1) | ||
11 | |||
12 | const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name') | ||
13 | expect(adminNameSettings).to.not.be.undefined | ||
14 | } | ||
15 | |||
16 | export { | ||
17 | testHelloWorldRegisteredSettings | ||
18 | } | ||
diff --git a/packages/tests/src/shared/requests.ts b/packages/tests/src/shared/requests.ts new file mode 100644 index 000000000..fc70ad6ed --- /dev/null +++ b/packages/tests/src/shared/requests.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { doRequest } from '@peertube/peertube-server/server/helpers/requests.js' | ||
2 | |||
3 | export function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { | ||
4 | const options = { | ||
5 | method: 'POST' as 'POST', | ||
6 | json: body, | ||
7 | httpSignature, | ||
8 | headers | ||
9 | } | ||
10 | |||
11 | return doRequest(url, options) | ||
12 | } | ||
diff --git a/packages/tests/src/shared/sql-command.ts b/packages/tests/src/shared/sql-command.ts new file mode 100644 index 000000000..1c4f89351 --- /dev/null +++ b/packages/tests/src/shared/sql-command.ts | |||
@@ -0,0 +1,150 @@ | |||
1 | import { QueryTypes, Sequelize } from 'sequelize' | ||
2 | import { forceNumber } from '@peertube/peertube-core-utils' | ||
3 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
4 | |||
5 | export class SQLCommand { | ||
6 | private sequelize: Sequelize | ||
7 | |||
8 | constructor (private readonly server: PeerTubeServer) { | ||
9 | |||
10 | } | ||
11 | |||
12 | deleteAll (table: string) { | ||
13 | const seq = this.getSequelize() | ||
14 | |||
15 | const options = { type: QueryTypes.DELETE } | ||
16 | |||
17 | return seq.query(`DELETE FROM "${table}"`, options) | ||
18 | } | ||
19 | |||
20 | async getVideoShareCount () { | ||
21 | const [ { total } ] = await this.selectQuery<{ total: string }>(`SELECT COUNT(*) as total FROM "videoShare"`) | ||
22 | if (total === null) return 0 | ||
23 | |||
24 | return parseInt(total, 10) | ||
25 | } | ||
26 | |||
27 | async getInternalFileUrl (fileId: number) { | ||
28 | return this.selectQuery<{ fileUrl: string }>(`SELECT "fileUrl" FROM "videoFile" WHERE id = :fileId`, { fileId }) | ||
29 | .then(rows => rows[0].fileUrl) | ||
30 | } | ||
31 | |||
32 | setActorField (to: string, field: string, value: string) { | ||
33 | return this.updateQuery(`UPDATE actor SET ${this.escapeColumnName(field)} = :value WHERE url = :to`, { value, to }) | ||
34 | } | ||
35 | |||
36 | setVideoField (uuid: string, field: string, value: string) { | ||
37 | return this.updateQuery(`UPDATE video SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid }) | ||
38 | } | ||
39 | |||
40 | setPlaylistField (uuid: string, field: string, value: string) { | ||
41 | return this.updateQuery(`UPDATE "videoPlaylist" SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid }) | ||
42 | } | ||
43 | |||
44 | async countVideoViewsOf (uuid: string) { | ||
45 | const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' + | ||
46 | `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = :uuid` | ||
47 | |||
48 | const [ { total } ] = await this.selectQuery<{ total: number }>(query, { uuid }) | ||
49 | if (!total) return 0 | ||
50 | |||
51 | return forceNumber(total) | ||
52 | } | ||
53 | |||
54 | getActorImage (filename: string) { | ||
55 | return this.selectQuery<{ width: number, height: number }>(`SELECT * FROM "actorImage" WHERE filename = :filename`, { filename }) | ||
56 | .then(rows => rows[0]) | ||
57 | } | ||
58 | |||
59 | // --------------------------------------------------------------------------- | ||
60 | |||
61 | setPluginVersion (pluginName: string, newVersion: string) { | ||
62 | return this.setPluginField(pluginName, 'version', newVersion) | ||
63 | } | ||
64 | |||
65 | setPluginLatestVersion (pluginName: string, newVersion: string) { | ||
66 | return this.setPluginField(pluginName, 'latestVersion', newVersion) | ||
67 | } | ||
68 | |||
69 | setPluginField (pluginName: string, field: string, value: string) { | ||
70 | return this.updateQuery( | ||
71 | `UPDATE "plugin" SET ${this.escapeColumnName(field)} = :value WHERE "name" = :pluginName`, | ||
72 | { pluginName, value } | ||
73 | ) | ||
74 | } | ||
75 | |||
76 | // --------------------------------------------------------------------------- | ||
77 | |||
78 | selectQuery <T extends object> (query: string, replacements: { [id: string]: string | number } = {}) { | ||
79 | const seq = this.getSequelize() | ||
80 | const options = { | ||
81 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
82 | replacements | ||
83 | } | ||
84 | |||
85 | return seq.query<T>(query, options) | ||
86 | } | ||
87 | |||
88 | updateQuery (query: string, replacements: { [id: string]: string | number } = {}) { | ||
89 | const seq = this.getSequelize() | ||
90 | const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE, replacements } | ||
91 | |||
92 | return seq.query(query, options) | ||
93 | } | ||
94 | |||
95 | // --------------------------------------------------------------------------- | ||
96 | |||
97 | async getPlaylistInfohash (playlistId: number) { | ||
98 | const query = 'SELECT "p2pMediaLoaderInfohashes" FROM "videoStreamingPlaylist" WHERE id = :playlistId' | ||
99 | |||
100 | const result = await this.selectQuery<{ p2pMediaLoaderInfohashes: string }>(query, { playlistId }) | ||
101 | if (!result || result.length === 0) return [] | ||
102 | |||
103 | return result[0].p2pMediaLoaderInfohashes | ||
104 | } | ||
105 | |||
106 | // --------------------------------------------------------------------------- | ||
107 | |||
108 | setActorFollowScores (newScore: number) { | ||
109 | return this.updateQuery(`UPDATE "actorFollow" SET "score" = :newScore`, { newScore }) | ||
110 | } | ||
111 | |||
112 | setTokenField (accessToken: string, field: string, value: string) { | ||
113 | return this.updateQuery( | ||
114 | `UPDATE "oAuthToken" SET ${this.escapeColumnName(field)} = :value WHERE "accessToken" = :accessToken`, | ||
115 | { value, accessToken } | ||
116 | ) | ||
117 | } | ||
118 | |||
119 | async cleanup () { | ||
120 | if (!this.sequelize) return | ||
121 | |||
122 | await this.sequelize.close() | ||
123 | this.sequelize = undefined | ||
124 | } | ||
125 | |||
126 | private getSequelize () { | ||
127 | if (this.sequelize) return this.sequelize | ||
128 | |||
129 | const dbname = 'peertube_test' + this.server.internalServerNumber | ||
130 | const username = 'peertube' | ||
131 | const password = 'peertube' | ||
132 | const host = '127.0.0.1' | ||
133 | const port = 5432 | ||
134 | |||
135 | this.sequelize = new Sequelize(dbname, username, password, { | ||
136 | dialect: 'postgres', | ||
137 | host, | ||
138 | port, | ||
139 | logging: false | ||
140 | }) | ||
141 | |||
142 | return this.sequelize | ||
143 | } | ||
144 | |||
145 | private escapeColumnName (columnName: string) { | ||
146 | return this.getSequelize().escape(columnName) | ||
147 | .replace(/^'/, '"') | ||
148 | .replace(/'$/, '"') | ||
149 | } | ||
150 | } | ||
diff --git a/packages/tests/src/shared/streaming-playlists.ts b/packages/tests/src/shared/streaming-playlists.ts new file mode 100644 index 000000000..f2f0fbe85 --- /dev/null +++ b/packages/tests/src/shared/streaming-playlists.ts | |||
@@ -0,0 +1,302 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename, dirname, join } from 'path' | ||
5 | import { removeFragmentedMP4Ext, uuidRegex } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | HttpStatusCode, | ||
8 | VideoPrivacy, | ||
9 | VideoResolution, | ||
10 | VideoStreamingPlaylist, | ||
11 | VideoStreamingPlaylistType | ||
12 | } from '@peertube/peertube-models' | ||
13 | import { sha256 } from '@peertube/peertube-node-utils' | ||
14 | import { makeRawRequest, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
15 | import { expectStartWith } from './checks.js' | ||
16 | import { hlsInfohashExist } from './tracker.js' | ||
17 | import { checkWebTorrentWorks } from './webtorrent.js' | ||
18 | |||
19 | async function checkSegmentHash (options: { | ||
20 | server: PeerTubeServer | ||
21 | baseUrlPlaylist: string | ||
22 | baseUrlSegment: string | ||
23 | resolution: number | ||
24 | hlsPlaylist: VideoStreamingPlaylist | ||
25 | token?: string | ||
26 | }) { | ||
27 | const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist, token } = options | ||
28 | const command = server.streamingPlaylists | ||
29 | |||
30 | const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) | ||
31 | const videoName = basename(file.fileUrl) | ||
32 | |||
33 | const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8`, token }) | ||
34 | |||
35 | const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) | ||
36 | |||
37 | const length = parseInt(matches[1], 10) | ||
38 | const offset = parseInt(matches[2], 10) | ||
39 | const range = `${offset}-${offset + length - 1}` | ||
40 | |||
41 | const segmentBody = await command.getFragmentedSegment({ | ||
42 | url: `${baseUrlSegment}/${videoName}`, | ||
43 | expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206, | ||
44 | range: `bytes=${range}`, | ||
45 | token | ||
46 | }) | ||
47 | |||
48 | const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, token }) | ||
49 | expect(sha256(segmentBody)).to.equal(shaBody[videoName][range], `Invalid sha256 result for ${videoName} range ${range}`) | ||
50 | } | ||
51 | |||
52 | // --------------------------------------------------------------------------- | ||
53 | |||
54 | async function checkLiveSegmentHash (options: { | ||
55 | server: PeerTubeServer | ||
56 | baseUrlSegment: string | ||
57 | videoUUID: string | ||
58 | segmentName: string | ||
59 | hlsPlaylist: VideoStreamingPlaylist | ||
60 | withRetry?: boolean | ||
61 | }) { | ||
62 | const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist, withRetry = false } = options | ||
63 | const command = server.streamingPlaylists | ||
64 | |||
65 | const segmentBody = await command.getFragmentedSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}`, withRetry }) | ||
66 | const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry }) | ||
67 | |||
68 | expect(sha256(segmentBody)).to.equal(shaBody[segmentName]) | ||
69 | } | ||
70 | |||
71 | // --------------------------------------------------------------------------- | ||
72 | |||
73 | async function checkResolutionsInMasterPlaylist (options: { | ||
74 | server: PeerTubeServer | ||
75 | playlistUrl: string | ||
76 | resolutions: number[] | ||
77 | token?: string | ||
78 | transcoded?: boolean // default true | ||
79 | withRetry?: boolean // default false | ||
80 | }) { | ||
81 | const { server, playlistUrl, resolutions, token, withRetry = false, transcoded = true } = options | ||
82 | |||
83 | const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, token, withRetry }) | ||
84 | |||
85 | for (const resolution of resolutions) { | ||
86 | const base = '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution | ||
87 | |||
88 | if (resolution === VideoResolution.H_NOVIDEO) { | ||
89 | expect(masterPlaylist).to.match(new RegExp(`${base},CODECS="mp4a.40.2"`)) | ||
90 | } else if (transcoded) { | ||
91 | expect(masterPlaylist).to.match(new RegExp(`${base},(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"`)) | ||
92 | } else { | ||
93 | expect(masterPlaylist).to.match(new RegExp(`${base}`)) | ||
94 | } | ||
95 | } | ||
96 | |||
97 | const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH=')) | ||
98 | expect(playlistsLength).to.have.lengthOf(resolutions.length) | ||
99 | } | ||
100 | |||
101 | async function completeCheckHlsPlaylist (options: { | ||
102 | servers: PeerTubeServer[] | ||
103 | videoUUID: string | ||
104 | hlsOnly: boolean | ||
105 | |||
106 | resolutions?: number[] | ||
107 | objectStorageBaseUrl?: string | ||
108 | }) { | ||
109 | const { videoUUID, hlsOnly, objectStorageBaseUrl } = options | ||
110 | |||
111 | const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ] | ||
112 | |||
113 | for (const server of options.servers) { | ||
114 | const videoDetails = await server.videos.getWithToken({ id: videoUUID }) | ||
115 | const requiresAuth = videoDetails.privacy.id === VideoPrivacy.PRIVATE || videoDetails.privacy.id === VideoPrivacy.INTERNAL | ||
116 | |||
117 | const privatePath = requiresAuth | ||
118 | ? 'private/' | ||
119 | : '' | ||
120 | const token = requiresAuth | ||
121 | ? server.accessToken | ||
122 | : undefined | ||
123 | |||
124 | const baseUrl = `http://${videoDetails.account.host}` | ||
125 | |||
126 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
127 | |||
128 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
129 | expect(hlsPlaylist).to.not.be.undefined | ||
130 | |||
131 | const hlsFiles = hlsPlaylist.files | ||
132 | expect(hlsFiles).to.have.lengthOf(resolutions.length) | ||
133 | |||
134 | if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0) | ||
135 | else expect(videoDetails.files).to.have.lengthOf(resolutions.length) | ||
136 | |||
137 | // Check JSON files | ||
138 | for (const resolution of resolutions) { | ||
139 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
140 | expect(file).to.not.be.undefined | ||
141 | |||
142 | if (file.resolution.id === VideoResolution.H_NOVIDEO) { | ||
143 | expect(file.resolution.label).to.equal('Audio') | ||
144 | } else { | ||
145 | expect(file.resolution.label).to.equal(resolution + 'p') | ||
146 | } | ||
147 | |||
148 | expect(file.magnetUri).to.have.lengthOf.above(2) | ||
149 | await checkWebTorrentWorks(file.magnetUri) | ||
150 | |||
151 | { | ||
152 | const nameReg = `${uuidRegex}-${file.resolution.id}` | ||
153 | |||
154 | expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}-hls.torrent`)) | ||
155 | |||
156 | if (objectStorageBaseUrl && requiresAuth) { | ||
157 | // eslint-disable-next-line max-len | ||
158 | expect(file.fileUrl).to.match(new RegExp(`${server.url}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`)) | ||
159 | } else if (objectStorageBaseUrl) { | ||
160 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | ||
161 | } else { | ||
162 | expect(file.fileUrl).to.match( | ||
163 | new RegExp(`${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`) | ||
164 | ) | ||
165 | } | ||
166 | } | ||
167 | |||
168 | { | ||
169 | await Promise.all([ | ||
170 | makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
171 | makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
172 | makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
173 | makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
174 | |||
175 | makeRawRequest({ | ||
176 | url: file.fileDownloadUrl, | ||
177 | token, | ||
178 | expectedStatus: objectStorageBaseUrl | ||
179 | ? HttpStatusCode.FOUND_302 | ||
180 | : HttpStatusCode.OK_200 | ||
181 | }) | ||
182 | ]) | ||
183 | } | ||
184 | } | ||
185 | |||
186 | // Check master playlist | ||
187 | { | ||
188 | await checkResolutionsInMasterPlaylist({ server, token, playlistUrl: hlsPlaylist.playlistUrl, resolutions }) | ||
189 | |||
190 | const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl, token }) | ||
191 | |||
192 | let i = 0 | ||
193 | for (const resolution of resolutions) { | ||
194 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
195 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
196 | |||
197 | const url = 'http://' + videoDetails.account.host | ||
198 | await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i) | ||
199 | |||
200 | i++ | ||
201 | } | ||
202 | } | ||
203 | |||
204 | // Check resolution playlists | ||
205 | { | ||
206 | for (const resolution of resolutions) { | ||
207 | const file = hlsFiles.find(f => f.resolution.id === resolution) | ||
208 | const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8' | ||
209 | |||
210 | let url: string | ||
211 | if (objectStorageBaseUrl && requiresAuth) { | ||
212 | url = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}` | ||
213 | } else if (objectStorageBaseUrl) { | ||
214 | url = `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}` | ||
215 | } else { | ||
216 | url = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}` | ||
217 | } | ||
218 | |||
219 | const subPlaylist = await server.streamingPlaylists.get({ url, token }) | ||
220 | |||
221 | expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) | ||
222 | expect(subPlaylist).to.contain(basename(file.fileUrl)) | ||
223 | } | ||
224 | } | ||
225 | |||
226 | { | ||
227 | let baseUrlAndPath: string | ||
228 | if (objectStorageBaseUrl && requiresAuth) { | ||
229 | baseUrlAndPath = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}` | ||
230 | } else if (objectStorageBaseUrl) { | ||
231 | baseUrlAndPath = `${objectStorageBaseUrl}hls/${videoUUID}` | ||
232 | } else { | ||
233 | baseUrlAndPath = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}` | ||
234 | } | ||
235 | |||
236 | for (const resolution of resolutions) { | ||
237 | await checkSegmentHash({ | ||
238 | server, | ||
239 | token, | ||
240 | baseUrlPlaylist: baseUrlAndPath, | ||
241 | baseUrlSegment: baseUrlAndPath, | ||
242 | resolution, | ||
243 | hlsPlaylist | ||
244 | }) | ||
245 | } | ||
246 | } | ||
247 | } | ||
248 | } | ||
249 | |||
250 | async function checkVideoFileTokenReinjection (options: { | ||
251 | server: PeerTubeServer | ||
252 | videoUUID: string | ||
253 | videoFileToken: string | ||
254 | resolutions: number[] | ||
255 | isLive: boolean | ||
256 | }) { | ||
257 | const { server, resolutions, videoFileToken, videoUUID, isLive } = options | ||
258 | |||
259 | const video = await server.videos.getWithToken({ id: videoUUID }) | ||
260 | const hls = video.streamingPlaylists[0] | ||
261 | |||
262 | const query = { videoFileToken, reinjectVideoFileToken: 'true' } | ||
263 | const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
264 | |||
265 | for (let i = 0; i < resolutions.length; i++) { | ||
266 | const resolution = resolutions[i] | ||
267 | |||
268 | const suffix = isLive | ||
269 | ? i | ||
270 | : `-${resolution}` | ||
271 | |||
272 | expect(text).to.contain(`${suffix}.m3u8?videoFileToken=${videoFileToken}&reinjectVideoFileToken=true`) | ||
273 | } | ||
274 | |||
275 | const resolutionPlaylists = extractResolutionPlaylistUrls(hls.playlistUrl, text) | ||
276 | expect(resolutionPlaylists).to.have.lengthOf(resolutions.length) | ||
277 | |||
278 | for (const url of resolutionPlaylists) { | ||
279 | const { text } = await makeRawRequest({ url, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
280 | |||
281 | const extension = isLive | ||
282 | ? '.ts' | ||
283 | : '.mp4' | ||
284 | |||
285 | expect(text).to.contain(`${extension}?videoFileToken=${videoFileToken}`) | ||
286 | expect(text).not.to.contain(`reinjectVideoFileToken=true`) | ||
287 | } | ||
288 | } | ||
289 | |||
290 | function extractResolutionPlaylistUrls (masterPath: string, masterContent: string) { | ||
291 | return masterContent.match(/^([^.]+\.m3u8.*)/mg) | ||
292 | .map(filename => join(dirname(masterPath), filename)) | ||
293 | } | ||
294 | |||
295 | export { | ||
296 | checkSegmentHash, | ||
297 | checkLiveSegmentHash, | ||
298 | checkResolutionsInMasterPlaylist, | ||
299 | completeCheckHlsPlaylist, | ||
300 | extractResolutionPlaylistUrls, | ||
301 | checkVideoFileTokenReinjection | ||
302 | } | ||
diff --git a/packages/tests/src/shared/tests.ts b/packages/tests/src/shared/tests.ts new file mode 100644 index 000000000..d2cb040fb --- /dev/null +++ b/packages/tests/src/shared/tests.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | const FIXTURE_URLS = { | ||
2 | peertube_long: 'https://peertube2.cpy.re/videos/watch/122d093a-1ede-43bd-bd34-59d2931ffc5e', | ||
3 | peertube_short: 'https://peertube2.cpy.re/w/3fbif9S3WmtTP8gGsC5HBd', | ||
4 | |||
5 | youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM', | ||
6 | |||
7 | /** | ||
8 | * The video is used to check format-selection correctness wrt. HDR, | ||
9 | * which brings its own set of oddities outside of a MediaSource. | ||
10 | * | ||
11 | * The video needs to have the following format_ids: | ||
12 | * (which you can check by using `youtube-dl <url> -F`): | ||
13 | * - (webm vp9) | ||
14 | * - (mp4 avc1) | ||
15 | * - (webm vp9.2 HDR) | ||
16 | */ | ||
17 | youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4', | ||
18 | |||
19 | youtubeChannel: 'https://youtube.com/channel/UCtnlZdXv3-xQzxiqfn6cjIA', | ||
20 | youtubePlaylist: 'https://youtube.com/playlist?list=PLRGXHPrcPd2yc2KdswlAWOxIJ8G3vgy4h', | ||
21 | |||
22 | // eslint-disable-next-line max-len | ||
23 | magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Flazy-static%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4', | ||
24 | |||
25 | badVideo: 'https://download.cpy.re/peertube/bad_video.mp4', | ||
26 | goodVideo: 'https://download.cpy.re/peertube/good_video.mp4', | ||
27 | goodVideo720: 'https://download.cpy.re/peertube/good_video_720.mp4', | ||
28 | |||
29 | file4K: 'https://download.cpy.re/peertube/4k_file.txt' | ||
30 | } | ||
31 | |||
32 | function buildRequestStub (): any { | ||
33 | return { } | ||
34 | } | ||
35 | |||
36 | export { | ||
37 | FIXTURE_URLS, | ||
38 | |||
39 | buildRequestStub | ||
40 | } | ||
diff --git a/packages/tests/src/shared/tracker.ts b/packages/tests/src/shared/tracker.ts new file mode 100644 index 000000000..6ab430456 --- /dev/null +++ b/packages/tests/src/shared/tracker.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { sha1 } from '@peertube/peertube-node-utils' | ||
3 | import { makeGetRequest } from '@peertube/peertube-server-commands' | ||
4 | |||
5 | async function hlsInfohashExist (serverUrl: string, masterPlaylistUrl: string, fileNumber: number) { | ||
6 | const path = '/tracker/announce' | ||
7 | |||
8 | const infohash = sha1(`2${masterPlaylistUrl}+V${fileNumber}`) | ||
9 | |||
10 | // From bittorrent-tracker | ||
11 | const infohashBinary = escape(Buffer.from(infohash, 'hex').toString('binary')).replace(/[@*/+]/g, function (char) { | ||
12 | return '%' + char.charCodeAt(0).toString(16).toUpperCase() | ||
13 | }) | ||
14 | |||
15 | const res = await makeGetRequest({ | ||
16 | url: serverUrl, | ||
17 | path, | ||
18 | rawQuery: `peer_id=-WW0105-NkvYO/egUAr4&info_hash=${infohashBinary}&port=42100`, | ||
19 | expectedStatus: 200 | ||
20 | }) | ||
21 | |||
22 | expect(res.text).to.not.contain('failure') | ||
23 | } | ||
24 | |||
25 | export { | ||
26 | hlsInfohashExist | ||
27 | } | ||
diff --git a/packages/tests/src/shared/video-playlists.ts b/packages/tests/src/shared/video-playlists.ts new file mode 100644 index 000000000..81dc43ed6 --- /dev/null +++ b/packages/tests/src/shared/video-playlists.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { readdir } from 'fs/promises' | ||
3 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
4 | |||
5 | async function checkPlaylistFilesWereRemoved ( | ||
6 | playlistUUID: string, | ||
7 | server: PeerTubeServer, | ||
8 | directories = [ 'thumbnails' ] | ||
9 | ) { | ||
10 | for (const directory of directories) { | ||
11 | const directoryPath = server.getDirectoryPath(directory) | ||
12 | |||
13 | const files = await readdir(directoryPath) | ||
14 | for (const file of files) { | ||
15 | expect(file).to.not.contain(playlistUUID) | ||
16 | } | ||
17 | } | ||
18 | } | ||
19 | |||
20 | export { | ||
21 | checkPlaylistFilesWereRemoved | ||
22 | } | ||
diff --git a/packages/tests/src/shared/videos.ts b/packages/tests/src/shared/videos.ts new file mode 100644 index 000000000..9bdcbf058 --- /dev/null +++ b/packages/tests/src/shared/videos.ts | |||
@@ -0,0 +1,323 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { readdir } from 'fs/promises' | ||
6 | import { basename, join } from 'path' | ||
7 | import { pick, uuidRegex } from '@peertube/peertube-core-utils' | ||
8 | import { HttpStatusCode, HttpStatusCodeType, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@peertube/peertube-models' | ||
9 | import { | ||
10 | loadLanguages, | ||
11 | VIDEO_CATEGORIES, | ||
12 | VIDEO_LANGUAGES, | ||
13 | VIDEO_LICENCES, | ||
14 | VIDEO_PRIVACIES | ||
15 | } from '@peertube/peertube-server/server/initializers/constants.js' | ||
16 | import { getLowercaseExtension } from '@peertube/peertube-node-utils' | ||
17 | import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@peertube/peertube-server-commands' | ||
18 | import { dateIsValid, expectStartWith, testImageGeneratedByFFmpeg } from './checks.js' | ||
19 | import { checkWebTorrentWorks } from './webtorrent.js' | ||
20 | |||
21 | async function completeWebVideoFilesCheck (options: { | ||
22 | server: PeerTubeServer | ||
23 | originServer: PeerTubeServer | ||
24 | videoUUID: string | ||
25 | fixture: string | ||
26 | files: { | ||
27 | resolution: number | ||
28 | size?: number | ||
29 | }[] | ||
30 | objectStorageBaseUrl?: string | ||
31 | }) { | ||
32 | const { originServer, server, videoUUID, files, fixture, objectStorageBaseUrl } = options | ||
33 | const video = await server.videos.getWithToken({ id: videoUUID }) | ||
34 | const serverConfig = await originServer.config.getConfig() | ||
35 | const requiresAuth = video.privacy.id === VideoPrivacy.PRIVATE || video.privacy.id === VideoPrivacy.INTERNAL | ||
36 | |||
37 | const transcodingEnabled = serverConfig.transcoding.web_videos.enabled | ||
38 | |||
39 | for (const attributeFile of files) { | ||
40 | const file = video.files.find(f => f.resolution.id === attributeFile.resolution) | ||
41 | expect(file, `resolution ${attributeFile.resolution} does not exist`).not.to.be.undefined | ||
42 | |||
43 | let extension = getLowercaseExtension(fixture) | ||
44 | // Transcoding enabled: extension will always be .mp4 | ||
45 | if (transcodingEnabled) extension = '.mp4' | ||
46 | |||
47 | expect(file.id).to.exist | ||
48 | expect(file.magnetUri).to.have.lengthOf.above(2) | ||
49 | |||
50 | { | ||
51 | const privatePath = requiresAuth | ||
52 | ? 'private/' | ||
53 | : '' | ||
54 | const nameReg = `${uuidRegex}-${file.resolution.id}` | ||
55 | |||
56 | expect(file.torrentDownloadUrl).to.match(new RegExp(`${server.url}/download/torrents/${nameReg}.torrent`)) | ||
57 | expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}.torrent`)) | ||
58 | |||
59 | if (objectStorageBaseUrl && requiresAuth) { | ||
60 | const regexp = new RegExp(`${originServer.url}/object-storage-proxy/web-videos/${privatePath}${nameReg}${extension}`) | ||
61 | expect(file.fileUrl).to.match(regexp) | ||
62 | } else if (objectStorageBaseUrl) { | ||
63 | expectStartWith(file.fileUrl, objectStorageBaseUrl) | ||
64 | } else { | ||
65 | expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/web-videos/${privatePath}${nameReg}${extension}`)) | ||
66 | } | ||
67 | |||
68 | expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`)) | ||
69 | } | ||
70 | |||
71 | { | ||
72 | const token = requiresAuth | ||
73 | ? server.accessToken | ||
74 | : undefined | ||
75 | |||
76 | await Promise.all([ | ||
77 | makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
78 | makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
79 | makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
80 | makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }), | ||
81 | makeRawRequest({ | ||
82 | url: file.fileDownloadUrl, | ||
83 | token, | ||
84 | expectedStatus: objectStorageBaseUrl ? HttpStatusCode.FOUND_302 : HttpStatusCode.OK_200 | ||
85 | }) | ||
86 | ]) | ||
87 | } | ||
88 | |||
89 | expect(file.resolution.id).to.equal(attributeFile.resolution) | ||
90 | |||
91 | if (file.resolution.id === VideoResolution.H_NOVIDEO) { | ||
92 | expect(file.resolution.label).to.equal('Audio') | ||
93 | } else { | ||
94 | expect(file.resolution.label).to.equal(attributeFile.resolution + 'p') | ||
95 | } | ||
96 | |||
97 | if (attributeFile.size) { | ||
98 | const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) | ||
99 | const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) | ||
100 | expect( | ||
101 | file.size, | ||
102 | 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')' | ||
103 | ).to.be.above(minSize).and.below(maxSize) | ||
104 | } | ||
105 | |||
106 | await checkWebTorrentWorks(file.magnetUri) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | async function completeVideoCheck (options: { | ||
111 | server: PeerTubeServer | ||
112 | originServer: PeerTubeServer | ||
113 | videoUUID: string | ||
114 | attributes: { | ||
115 | name: string | ||
116 | category: number | ||
117 | licence: number | ||
118 | language: string | ||
119 | nsfw: boolean | ||
120 | commentsEnabled: boolean | ||
121 | downloadEnabled: boolean | ||
122 | description: string | ||
123 | publishedAt?: string | ||
124 | support: string | ||
125 | originallyPublishedAt?: string | ||
126 | account: { | ||
127 | name: string | ||
128 | host: string | ||
129 | } | ||
130 | isLocal: boolean | ||
131 | tags: string[] | ||
132 | privacy: number | ||
133 | likes?: number | ||
134 | dislikes?: number | ||
135 | duration: number | ||
136 | channel: { | ||
137 | displayName: string | ||
138 | name: string | ||
139 | description: string | ||
140 | isLocal: boolean | ||
141 | } | ||
142 | fixture: string | ||
143 | files: { | ||
144 | resolution: number | ||
145 | size: number | ||
146 | }[] | ||
147 | thumbnailfile?: string | ||
148 | previewfile?: string | ||
149 | } | ||
150 | }) { | ||
151 | const { attributes, originServer, server, videoUUID } = options | ||
152 | |||
153 | await loadLanguages() | ||
154 | |||
155 | const video = await server.videos.get({ id: videoUUID }) | ||
156 | |||
157 | if (!attributes.likes) attributes.likes = 0 | ||
158 | if (!attributes.dislikes) attributes.dislikes = 0 | ||
159 | |||
160 | expect(video.name).to.equal(attributes.name) | ||
161 | expect(video.category.id).to.equal(attributes.category) | ||
162 | expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Unknown') | ||
163 | expect(video.licence.id).to.equal(attributes.licence) | ||
164 | expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown') | ||
165 | expect(video.language.id).to.equal(attributes.language) | ||
166 | expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown') | ||
167 | expect(video.privacy.id).to.deep.equal(attributes.privacy) | ||
168 | expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy]) | ||
169 | expect(video.nsfw).to.equal(attributes.nsfw) | ||
170 | expect(video.description).to.equal(attributes.description) | ||
171 | expect(video.account.id).to.be.a('number') | ||
172 | expect(video.account.host).to.equal(attributes.account.host) | ||
173 | expect(video.account.name).to.equal(attributes.account.name) | ||
174 | expect(video.channel.displayName).to.equal(attributes.channel.displayName) | ||
175 | expect(video.channel.name).to.equal(attributes.channel.name) | ||
176 | expect(video.likes).to.equal(attributes.likes) | ||
177 | expect(video.dislikes).to.equal(attributes.dislikes) | ||
178 | expect(video.isLocal).to.equal(attributes.isLocal) | ||
179 | expect(video.duration).to.equal(attributes.duration) | ||
180 | expect(video.url).to.contain(originServer.host) | ||
181 | expect(dateIsValid(video.createdAt)).to.be.true | ||
182 | expect(dateIsValid(video.publishedAt)).to.be.true | ||
183 | expect(dateIsValid(video.updatedAt)).to.be.true | ||
184 | |||
185 | if (attributes.publishedAt) { | ||
186 | expect(video.publishedAt).to.equal(attributes.publishedAt) | ||
187 | } | ||
188 | |||
189 | if (attributes.originallyPublishedAt) { | ||
190 | expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt) | ||
191 | } else { | ||
192 | expect(video.originallyPublishedAt).to.be.null | ||
193 | } | ||
194 | |||
195 | expect(video.files).to.have.lengthOf(attributes.files.length) | ||
196 | expect(video.tags).to.deep.equal(attributes.tags) | ||
197 | expect(video.account.name).to.equal(attributes.account.name) | ||
198 | expect(video.account.host).to.equal(attributes.account.host) | ||
199 | expect(video.channel.displayName).to.equal(attributes.channel.displayName) | ||
200 | expect(video.channel.name).to.equal(attributes.channel.name) | ||
201 | expect(video.channel.host).to.equal(attributes.account.host) | ||
202 | expect(video.channel.isLocal).to.equal(attributes.channel.isLocal) | ||
203 | expect(video.channel.createdAt).to.exist | ||
204 | expect(dateIsValid(video.channel.updatedAt.toString())).to.be.true | ||
205 | expect(video.commentsEnabled).to.equal(attributes.commentsEnabled) | ||
206 | expect(video.downloadEnabled).to.equal(attributes.downloadEnabled) | ||
207 | |||
208 | expect(video.thumbnailPath).to.exist | ||
209 | await testImageGeneratedByFFmpeg(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath) | ||
210 | |||
211 | if (attributes.previewfile) { | ||
212 | expect(video.previewPath).to.exist | ||
213 | await testImageGeneratedByFFmpeg(server.url, attributes.previewfile, video.previewPath) | ||
214 | } | ||
215 | |||
216 | await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) }) | ||
217 | } | ||
218 | |||
219 | async function checkVideoFilesWereRemoved (options: { | ||
220 | server: PeerTubeServer | ||
221 | video: VideoDetails | ||
222 | captions?: VideoCaption[] | ||
223 | onlyVideoFiles?: boolean // default false | ||
224 | }) { | ||
225 | const { video, server, captions = [], onlyVideoFiles = false } = options | ||
226 | |||
227 | const webVideoFiles = video.files || [] | ||
228 | const hlsFiles = video.streamingPlaylists[0]?.files || [] | ||
229 | |||
230 | const thumbnailName = basename(video.thumbnailPath) | ||
231 | const previewName = basename(video.previewPath) | ||
232 | |||
233 | const torrentNames = webVideoFiles.concat(hlsFiles).map(f => basename(f.torrentUrl)) | ||
234 | |||
235 | const captionNames = captions.map(c => basename(c.captionPath)) | ||
236 | |||
237 | const webVideoFilenames = webVideoFiles.map(f => basename(f.fileUrl)) | ||
238 | const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl)) | ||
239 | |||
240 | let directories: { [ directory: string ]: string[] } = { | ||
241 | videos: webVideoFilenames, | ||
242 | redundancy: webVideoFilenames, | ||
243 | [join('playlists', 'hls')]: hlsFilenames, | ||
244 | [join('redundancy', 'hls')]: hlsFilenames | ||
245 | } | ||
246 | |||
247 | if (onlyVideoFiles !== true) { | ||
248 | directories = { | ||
249 | ...directories, | ||
250 | |||
251 | thumbnails: [ thumbnailName ], | ||
252 | previews: [ previewName ], | ||
253 | torrents: torrentNames, | ||
254 | captions: captionNames | ||
255 | } | ||
256 | } | ||
257 | |||
258 | for (const directory of Object.keys(directories)) { | ||
259 | const directoryPath = server.servers.buildDirectory(directory) | ||
260 | |||
261 | const directoryExists = await pathExists(directoryPath) | ||
262 | if (directoryExists === false) continue | ||
263 | |||
264 | const existingFiles = await readdir(directoryPath) | ||
265 | for (const existingFile of existingFiles) { | ||
266 | for (const shouldNotExist of directories[directory]) { | ||
267 | expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist) | ||
268 | } | ||
269 | } | ||
270 | } | ||
271 | } | ||
272 | |||
273 | async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) { | ||
274 | for (const server of servers) { | ||
275 | server.store.videoDetails = await server.videos.get({ id: uuid }) | ||
276 | } | ||
277 | } | ||
278 | |||
279 | function checkUploadVideoParam (options: { | ||
280 | server: PeerTubeServer | ||
281 | token: string | ||
282 | attributes: Partial<VideoEdit> | ||
283 | expectedStatus?: HttpStatusCodeType | ||
284 | completedExpectedStatus?: HttpStatusCodeType | ||
285 | mode?: 'legacy' | 'resumable' | ||
286 | }) { | ||
287 | const { server, token, attributes, completedExpectedStatus, expectedStatus, mode = 'legacy' } = options | ||
288 | |||
289 | return mode === 'legacy' | ||
290 | ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus: expectedStatus || completedExpectedStatus }) | ||
291 | : server.videos.buildResumeUpload({ | ||
292 | token, | ||
293 | attributes, | ||
294 | expectedStatus, | ||
295 | completedExpectedStatus, | ||
296 | path: '/api/v1/videos/upload-resumable' | ||
297 | }) | ||
298 | } | ||
299 | |||
300 | // serverNumber starts from 1 | ||
301 | async function uploadRandomVideoOnServers ( | ||
302 | servers: PeerTubeServer[], | ||
303 | serverNumber: number, | ||
304 | additionalParams?: VideoEdit & { prefixName?: string } | ||
305 | ) { | ||
306 | const server = servers.find(s => s.serverNumber === serverNumber) | ||
307 | const res = await server.videos.randomUpload({ wait: false, additionalParams }) | ||
308 | |||
309 | await waitJobs(servers) | ||
310 | |||
311 | return res | ||
312 | } | ||
313 | |||
314 | // --------------------------------------------------------------------------- | ||
315 | |||
316 | export { | ||
317 | completeVideoCheck, | ||
318 | completeWebVideoFilesCheck, | ||
319 | checkUploadVideoParam, | ||
320 | uploadRandomVideoOnServers, | ||
321 | checkVideoFilesWereRemoved, | ||
322 | saveVideoInServers | ||
323 | } | ||
diff --git a/packages/tests/src/shared/views.ts b/packages/tests/src/shared/views.ts new file mode 100644 index 000000000..b791eff25 --- /dev/null +++ b/packages/tests/src/shared/views.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | import type { FfmpegCommand } from 'fluent-ffmpeg' | ||
2 | import { wait } from '@peertube/peertube-core-utils' | ||
3 | import { VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | createMultipleServers, | ||
6 | doubleFollow, | ||
7 | PeerTubeServer, | ||
8 | setAccessTokensToServers, | ||
9 | setDefaultVideoChannel, | ||
10 | waitJobs, | ||
11 | waitUntilLivePublishedOnAllServers | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | async function processViewersStats (servers: PeerTubeServer[]) { | ||
15 | await wait(6000) | ||
16 | |||
17 | for (const server of servers) { | ||
18 | await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } }) | ||
19 | await server.debug.sendCommand({ body: { command: 'process-video-viewers' } }) | ||
20 | } | ||
21 | |||
22 | await waitJobs(servers) | ||
23 | } | ||
24 | |||
25 | async function processViewsBuffer (servers: PeerTubeServer[]) { | ||
26 | for (const server of servers) { | ||
27 | await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } }) | ||
28 | } | ||
29 | |||
30 | await waitJobs(servers) | ||
31 | } | ||
32 | |||
33 | async function prepareViewsServers () { | ||
34 | const servers = await createMultipleServers(2) | ||
35 | await setAccessTokensToServers(servers) | ||
36 | await setDefaultVideoChannel(servers) | ||
37 | |||
38 | await servers[0].config.updateCustomSubConfig({ | ||
39 | newConfig: { | ||
40 | live: { | ||
41 | enabled: true, | ||
42 | allowReplay: true, | ||
43 | transcoding: { | ||
44 | enabled: false | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | }) | ||
49 | |||
50 | await doubleFollow(servers[0], servers[1]) | ||
51 | |||
52 | return servers | ||
53 | } | ||
54 | |||
55 | async function prepareViewsVideos (options: { | ||
56 | servers: PeerTubeServer[] | ||
57 | live: boolean | ||
58 | vod: boolean | ||
59 | }) { | ||
60 | const { servers } = options | ||
61 | |||
62 | const liveAttributes = { | ||
63 | name: 'live video', | ||
64 | channelId: servers[0].store.channel.id, | ||
65 | privacy: VideoPrivacy.PUBLIC | ||
66 | } | ||
67 | |||
68 | let ffmpegCommand: FfmpegCommand | ||
69 | let live: VideoCreateResult | ||
70 | let vod: VideoCreateResult | ||
71 | |||
72 | if (options.live) { | ||
73 | live = await servers[0].live.create({ fields: liveAttributes }) | ||
74 | |||
75 | ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: live.uuid }) | ||
76 | await waitUntilLivePublishedOnAllServers(servers, live.uuid) | ||
77 | } | ||
78 | |||
79 | if (options.vod) { | ||
80 | vod = await servers[0].videos.quickUpload({ name: 'video' }) | ||
81 | } | ||
82 | |||
83 | await waitJobs(servers) | ||
84 | |||
85 | return { liveVideoId: live?.uuid, vodVideoId: vod?.uuid, ffmpegCommand } | ||
86 | } | ||
87 | |||
88 | export { | ||
89 | processViewersStats, | ||
90 | prepareViewsServers, | ||
91 | processViewsBuffer, | ||
92 | prepareViewsVideos | ||
93 | } | ||
diff --git a/packages/tests/src/shared/webtorrent.ts b/packages/tests/src/shared/webtorrent.ts new file mode 100644 index 000000000..1be54426a --- /dev/null +++ b/packages/tests/src/shared/webtorrent.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { readFile } from 'fs/promises' | ||
3 | import parseTorrent from 'parse-torrent' | ||
4 | import { basename, join } from 'path' | ||
5 | import type { Instance, Torrent } from 'webtorrent' | ||
6 | import { VideoFile } from '@peertube/peertube-models' | ||
7 | import { PeerTubeServer } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | let webtorrent: Instance | ||
10 | |||
11 | export async function checkWebTorrentWorks (magnetUri: string, pathMatch?: RegExp) { | ||
12 | const torrent = await webtorrentAdd(magnetUri, true) | ||
13 | |||
14 | expect(torrent.files).to.be.an('array') | ||
15 | expect(torrent.files.length).to.equal(1) | ||
16 | expect(torrent.files[0].path).to.exist.and.to.not.equal('') | ||
17 | |||
18 | if (pathMatch) { | ||
19 | expect(torrent.files[0].path).match(pathMatch) | ||
20 | } | ||
21 | } | ||
22 | |||
23 | export async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) { | ||
24 | const torrentName = basename(file.torrentUrl) | ||
25 | const torrentPath = server.servers.buildDirectory(join('torrents', torrentName)) | ||
26 | |||
27 | const data = await readFile(torrentPath) | ||
28 | |||
29 | return parseTorrent(data) | ||
30 | } | ||
31 | |||
32 | // --------------------------------------------------------------------------- | ||
33 | // Private | ||
34 | // --------------------------------------------------------------------------- | ||
35 | |||
36 | async function webtorrentAdd (torrentId: string, refreshWebTorrent = false) { | ||
37 | const WebTorrent = (await import('webtorrent')).default | ||
38 | |||
39 | if (webtorrent && refreshWebTorrent) webtorrent.destroy() | ||
40 | if (!webtorrent || refreshWebTorrent) webtorrent = new WebTorrent() | ||
41 | |||
42 | webtorrent.on('error', err => console.error('Error in webtorrent', err)) | ||
43 | |||
44 | return new Promise<Torrent>(res => { | ||
45 | const torrent = webtorrent.add(torrentId, res) | ||
46 | |||
47 | torrent.on('error', err => console.error('Error in webtorrent torrent', err)) | ||
48 | torrent.on('warning', warn => { | ||
49 | const msg = typeof warn === 'string' | ||
50 | ? warn | ||
51 | : warn.message | ||
52 | |||
53 | if (msg.includes('Unsupported')) return | ||
54 | |||
55 | console.error('Warning in webtorrent torrent', warn) | ||
56 | }) | ||
57 | }) | ||
58 | } | ||
diff --git a/packages/tests/tsconfig.json b/packages/tests/tsconfig.json new file mode 100644 index 000000000..91a74b4be --- /dev/null +++ b/packages/tests/tsconfig.json | |||
@@ -0,0 +1,27 @@ | |||
1 | { | ||
2 | "extends": "../../tsconfig.base.json", | ||
3 | "compilerOptions": { | ||
4 | "outDir": "./dist", | ||
5 | "baseUrl": "./", | ||
6 | "rootDir": "src", | ||
7 | "tsBuildInfoFile": "./dist/.tsbuildinfo", | ||
8 | "paths": { | ||
9 | "@tests/*": [ "src/*" ] | ||
10 | } | ||
11 | }, | ||
12 | "references": [ | ||
13 | { "path": "../core-utils" }, | ||
14 | { "path": "../ffmpeg" }, | ||
15 | { "path": "../models" }, | ||
16 | { "path": "../node-utils" }, | ||
17 | { "path": "../typescript-utils" }, | ||
18 | { "path": "../server-commands" }, | ||
19 | { "path": "../../server/tsconfig.lib.json" } | ||
20 | ], | ||
21 | "include": [ | ||
22 | "./src/**/*.ts" | ||
23 | ], | ||
24 | "exclude": [ | ||
25 | "./fixtures" | ||
26 | ] | ||
27 | } | ||