Skip to content

Commit 2bb803a

Browse files
authored
Updated code to work with new DB rules, better tests (#214)
1 parent c47e48b commit 2bb803a

File tree

8 files changed

+99
-27
lines changed

8 files changed

+99
-27
lines changed

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# Changelog
22

3-
## v2.2.2
3+
## v2.2.3
44

55
<!--Releasenotes start-->
6+
- Error messages now include the current channel ID to help with reporting an issue.
7+
- Some changes in how data that is sent to the database is handled, to prepare for a future security update.
8+
<!--Releasenotes end-->
9+
10+
## v2.2.2
11+
612
- Fixed a bug where there would be no video IDs stored in the database, leading to an error.
713
- Fixed the 'Changelog' button on the changelog page not doing anything.
8-
<!--Releasenotes end-->
914

1015
## v2.2.1
1116

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "random-youtube-video",
3-
"version": "2.2.2",
3+
"version": "2.2.3",
44
"description": "Play a random video uploaded on the current YouTube channel.",
55
"scripts": {
66
"dev": "concurrently \"npm run dev:chromium\" \"npm run dev:firefox\"",

src/background.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
138138
// Updates (overwriting videos) a playlist in Firebase
139139
case "overwritePlaylistInfoInDB":
140140
updatePlaylistInfoInDB('uploadsPlaylists/' + request.data.key, request.data.val, true).then(sendResponse);
141-
break;
141+
break;
142142
// Before v1.0.0 the videos were stored in an array without upload times, so they need to all be refetched
143143
case 'updateDBPlaylistToV1.0.0':
144144
updateDBPlaylistToV1_0_0('uploadsPlaylists/' + request.data.key).then(sendResponse);
@@ -189,7 +189,14 @@ const app = initializeApp(firebaseConfig);
189189
const db = getDatabase(app);
190190

191191
async function updatePlaylistInfoInDB(playlistId, playlistInfo, overwriteVideos) {
192-
if (overwriteVideos) {
192+
// Find out if the playlist already exists in the database
193+
// We only need to send this request if we don't already have to overwrite the entry
194+
let playlistExists = true;
195+
if (!overwriteVideos) {
196+
playlistExists = await readDataOnce(playlistId);
197+
}
198+
199+
if (overwriteVideos || !playlistExists) {
193200
console.log("Setting playlistInfo in the database...");
194201
// Update the entire object. Due to the way Firebase works, this will overwrite the existing 'videos' object, as it is nested within the playlist
195202
update(ref(db, playlistId), playlistInfo);

src/content.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ function resetShuffleButtonText() {
120120
// ---------- Shuffle ----------
121121
// Called when the 'Shuffle' button is clicked
122122
async function shuffleVideos() {
123+
let channelId;
123124
try {
124125
// Get the saved channelId from the button
125-
const channelId = shuffleButton?.children[0]?.children[0]?.children[0]?.children?.namedItem('channelId')?.innerText;
126+
channelId = shuffleButton?.children[0]?.children[0]?.children[0]?.children?.namedItem('channelId')?.innerText;
126127

127128
// If the channelId somehow wasn't saved, throw an error
128129
if (!channelId) {
@@ -179,7 +180,7 @@ The page will reload and you can try again.`)
179180
}
180181

181182
// Alert the user about the error
182-
window.alert(`Random YouTube Video:\n\n${displayText}${error.message ? "\n" + error.message : ""}${error.reason ? "\n" + error.reason : ""}${error.solveHint ? "\n" + error.solveHint : ""}${error.showTrace !== false ? "\n\n" + error.stack : ""}`);
183+
window.alert(`Random YouTube Video:\n\nChannel ${channelId}\n${displayText}${error.message ? "\n" + error.message : ""}${error.reason ? "\n" + error.reason : ""}${error.solveHint ? "\n" + error.solveHint : ""}${error.showTrace !== false ? "\n\n" + error.stack : ""}`);
183184

184185
// Immediately display the error
185186
shuffleButtonTextElement.innerText = `\xa0${displayText}`;

src/shuffleVideo.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ export async function chooseRandomVideo(channelId, firedFromPopup, progressTextE
131131
} else {
132132
// Otherwise, we want to only upload new videos. If there are no "newVideos", we upload all videos, as this is the first time we are uploading the playlist
133133
console.log("Uploading new video IDs to the database...");
134-
if(getLength(playlistInfo["newVideos"] ?? {}) > 0) {
134+
if (getLength(playlistInfo["newVideos"] ?? {}) > 0) {
135135
videosToDatabase = playlistInfo["newVideos"];
136136
} else {
137-
videosToDatabase = playlistInfo["videos"] ?? 0;
137+
videosToDatabase = playlistInfo["videos"];
138138
}
139139
}
140140

@@ -155,7 +155,7 @@ export async function chooseRandomVideo(channelId, firedFromPopup, progressTextE
155155
// Remember the last time the playlist was accessed locally (==now)
156156
"lastAccessedLocally": new Date().toISOString(),
157157
"lastFetchedFromDB": playlistInfo["lastFetchedFromDB"] ?? new Date(0).toISOString(),
158-
"lastVideoPublishedAt": playlistInfo["lastVideoPublishedAt"] ?? new Date(0).toISOString(),
158+
"lastVideoPublishedAt": playlistInfo["lastVideoPublishedAt"] ?? new Date(0).toISOString().slice(0, 19) + 'Z',
159159
"videos": playlistInfo["videos"] ?? {}
160160
};
161161

@@ -225,10 +225,24 @@ async function uploadPlaylistToDatabase(playlistInfo, videosToDatabase, uploadsP
225225
// Only upload the wanted keys
226226
const playlistInfoForDatabase = {
227227
"lastUpdatedDBAt": playlistInfo["lastUpdatedDBAt"] ?? new Date().toISOString(),
228-
"lastVideoPublishedAt": playlistInfo["lastVideoPublishedAt"] ?? new Date(0).toISOString(),
228+
"lastVideoPublishedAt": playlistInfo["lastVideoPublishedAt"] ?? new Date(0).toISOString().slice(0, 19) + 'Z',
229229
"videos": videosToDatabase
230230
};
231231

232+
// Make sure the data is in the correct format
233+
if (playlistInfoForDatabase["lastUpdatedDBAt"].length !== 24) {
234+
alert(`Random YouTube Video:\nPlease send this information to the developer:\n\nlastUpdatedDBAt has the wrong format (got ${playlistInfoForDatabase["lastVideoPublishedAt"]}).\nChannelId: ${uploadsPlaylistId}.`);
235+
return;
236+
}
237+
if (playlistInfoForDatabase["lastVideoPublishedAt"].length !== 20) {
238+
alert(`Random YouTube Video:\nPlease send this information to the developer:\n\nlastVideoPublishedAt has the wrong format (got ${playlistInfoForDatabase["lastVideoPublishedAt"]}).\nChannelId: ${uploadsPlaylistId}.`);
239+
return;
240+
}
241+
if (getLength(playlistInfoForDatabase["videos"]) < 1) {
242+
alert(`Random YouTube Video:\nPlease send this information to the developer:\n\nNo videos object was found.\nChannelId: ${uploadsPlaylistId}.`);
243+
return;
244+
}
245+
232246
// Send the playlist info to the database
233247
const msg = {
234248
command: encounteredDeletedVideos ? 'overwritePlaylistInfoInDB' : 'updatePlaylistInfoInDB',
@@ -916,12 +930,14 @@ async function aprilFoolsJoke() {
916930
async function tryGetPlaylistFromLocalStorage(playlistId) {
917931
return await chrome.storage.local.get([playlistId]).then(async (result) => {
918932
if (result[playlistId]) {
933+
/* c8 ignore start */
919934
// To fix a bug introduced in v2.2.1
920-
if(!result[playlistId]["videos"]) {
935+
if (!result[playlistId]["videos"]) {
921936
// Remove from localStorage
922937
await chrome.storage.local.remove([playlistId]);
923938
return {}
924939
}
940+
/* c8 ignore stop */
925941
return result[playlistId];
926942
}
927943
return {};

static/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "Random YouTube Video",
33
"description": "Play a random video uploaded on the current YouTube channel.",
4-
"version": "2.2.2",
4+
"version": "2.2.3",
55
"manifest_version": 3,
66
"content_scripts": [
77
{

test/playlistPermutations.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -272,19 +272,19 @@ const defaultDBDeletedVideos = {
272272

273273
// The YT API returns a full-length timestamp
274274
const oneNewYTAPIVideo = {
275-
"YT_V_000001": zeroDaysAgo
275+
"YT_V_000001": zeroDaysAgo.slice(0, 19) + 'Z'
276276
};
277277

278278
// Get over the 50 per page API limit, and get to more than one additional page for the inner while loop
279279
const multipleNewYTAPIVideos = {};
280280
for (let i = 1; i <= 70; i++) {
281281
const key = `YT_V_${String(i).padStart(6, '0')}`;
282-
multipleNewYTAPIVideos[key] = zeroDaysAgo;
282+
multipleNewYTAPIVideos[key] = zeroDaysAgo.slice(0, 19) + 'Z';
283283
}
284284
// Add another 35 shorts, with _S_ in the key
285285
for (let i = 71; i <= 105; i++) {
286286
const key = `YT_S_${String(i).padStart(6, '0')}`;
287-
multipleNewYTAPIVideos[key] = zeroDaysAgo;
287+
multipleNewYTAPIVideos[key] = zeroDaysAgo.slice(0, 19) + 'Z';
288288
}
289289

290290
// ---------- Permutations for testing ----------
@@ -379,7 +379,7 @@ for (let i = 0; i < playlistModifiers[0].length; i++) {
379379
}
380380

381381
// When was the last locally known video published
382-
localLastVideoPublishedAt = threeDaysAgo;
382+
localLastVideoPublishedAt = threeDaysAgo.slice(0, 19) + 'Z';
383383

384384
if (playlistModifiers[3][l] === "LocalPlaylistContainsDeletedVideos") {
385385
localVideos = deepCopy(defaultLocalVideos);
@@ -401,7 +401,7 @@ for (let i = 0; i < playlistModifiers[0].length; i++) {
401401
if (playlistModifiers[5][n] === "DBContainsVideosNotInLocalPlaylist") {
402402
dbVideos = deepCopy({ ...defaultLocalVideos, ...defaultDBVideos });
403403
dbDeletedVideos = null;
404-
dbLastVideoPublishedAt = twoDaysAgo;
404+
dbLastVideoPublishedAt = twoDaysAgo.slice(0, 19) + 'Z';
405405
} else if (playlistModifiers[5][n] === "DBContainsNoVideosNotInLocalPlaylist") {
406406
dbVideos = playlistModifiers[3][l] === "LocalPlaylistContainsOnlyShorts"
407407
? deepCopy(defaultLocalShorts)
@@ -421,10 +421,10 @@ for (let i = 0; i < playlistModifiers[0].length; i++) {
421421
if (playlistModifiers[1][j] !== "DBEntryIsUpToDate") {
422422
if (playlistModifiers[4][m] === "OneNewVideoUploaded") {
423423
newUploadedVideos = deepCopy(oneNewYTAPIVideo);
424-
newLastVideoPublishedAt = zeroDaysAgo;
424+
newLastVideoPublishedAt = zeroDaysAgo.slice(0, 19) + 'Z';
425425
} else if (playlistModifiers[4][m] === "MultipleNewVideosUploaded") {
426426
newUploadedVideos = deepCopy(multipleNewYTAPIVideos);
427-
newLastVideoPublishedAt = zeroDaysAgo;
427+
newLastVideoPublishedAt = zeroDaysAgo.slice(0, 19) + 'Z';
428428
} else if (playlistModifiers[4][m] === "NoNewVideoUploaded") {
429429
newUploadedVideos = null;
430430
newLastVideoPublishedAt = dbLastVideoPublishedAt;

test/shuffleVideo.test.js

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ function setUpMockResponses(mockResponses) {
4242
});
4343
}
4444

45+
// Checks that a set of messages contains the correct data format for the database
46+
function checkPlaylistsUploadedToDB(messages, input) {
47+
messages.forEach((message) => {
48+
const data = message[0].data;
49+
50+
expect(message.length).to.be(1);
51+
52+
expect(data.key).to.be(input.playlistId);
53+
expect(Object.keys(data.val)).to.contain('lastUpdatedDBAt');
54+
expect(data.val.lastUpdatedDBAt.length).to.be(24);
55+
expect(Object.keys(data.val)).to.contain('lastVideoPublishedAt');
56+
expect(data.val.lastVideoPublishedAt.length).to.be(20);
57+
expect(Object.keys(data.val)).to.contain('videos');
58+
expect(Object.keys(data.val.videos).length).to.be.greaterThan(0);
59+
// Check the format of the videos
60+
for (const [videoId, publishTime] of Object.entries(data.val.videos)) {
61+
expect(videoId.length).to.be(11);
62+
expect(publishTime.length).to.be(10);
63+
}
64+
});
65+
}
66+
4567
describe('shuffleVideo', function () {
4668

4769
beforeEach(function () {
@@ -481,7 +503,8 @@ describe('shuffleVideo', function () {
481503
if (videoId.startsWith('DEL_LOC')) {
482504
delete allVideos[videoId];
483505
} else {
484-
allVideos[videoId] = publishTime;
506+
// The YT API returns the publishTime in ISO 8601 format, so we need to convert it, as the localStorage and DB use a different format
507+
allVideos[videoId] = new Date(publishTime).toISOString().slice(0, 19) + 'Z';
485508
}
486509
}
487510
allVideos = Object.fromEntries(Object.entries(allVideos).sort((a, b) => b[1].localeCompare(a[1])));
@@ -856,7 +879,8 @@ describe('shuffleVideo', function () {
856879
if (videoId.startsWith('DEL_LOC')) {
857880
delete allVideos[videoId];
858881
} else {
859-
allVideos[videoId] = publishTime;
882+
// The YT API returns the publishTime in ISO 8601 format, so we need to convert it, as the localStorage and DB use a different format
883+
allVideos[videoId] = new Date(publishTime).toISOString().slice(0, 19) + 'Z';
860884
}
861885
}
862886
allVideos = Object.fromEntries(Object.entries(allVideos).sort((a, b) => b[1].localeCompare(a[1])));
@@ -963,7 +987,8 @@ describe('shuffleVideo', function () {
963987
it('should only interact with the database to remove deleted videos', async function () {
964988
await chooseRandomVideo(input.channelId, false, domElement);
965989

966-
const commands = chrome.runtime.sendMessage.args.map(arg => arg[0].command);
990+
const messages = chrome.runtime.sendMessage.args;
991+
const commands = messages.map(arg => arg[0].command);
967992

968993
// callCount is 3 if we didn't choose a deleted video, 4 else
969994
expect(chrome.runtime.sendMessage.callCount).to.be.within(3, 4);
@@ -974,6 +999,9 @@ describe('shuffleVideo', function () {
974999

9751000
if (chrome.runtime.sendMessage.callCount === 4) {
9761001
expect(commands).to.contain('overwritePlaylistInfoInDB');
1002+
// One entry should contain a valid DB update
1003+
const overwriteMessages = messages.filter(arg => arg[0].command === 'overwritePlaylistInfoInDB');
1004+
checkPlaylistsUploadedToDB(overwriteMessages, input);
9771005
}
9781006

9791007
});
@@ -982,7 +1010,8 @@ describe('shuffleVideo', function () {
9821010
await chooseRandomVideo(input.channelId, false, domElement);
9831011
const playlistInfoAfter = await getKeyFromLocalStorage(input.playlistId);
9841012

985-
const commands = chrome.runtime.sendMessage.args.map(arg => arg[0].command);
1013+
const messages = chrome.runtime.sendMessage.args;
1014+
const commands = messages.map(arg => arg[0].command);
9861015

9871016
if (needsYTAPIInteraction(input)) {
9881017
expect(chrome.runtime.sendMessage.callCount).to.be(6);
@@ -1008,14 +1037,21 @@ describe('shuffleVideo', function () {
10081037
break;
10091038
case 5:
10101039
expect(commands).to.contain('overwritePlaylistInfoInDB');
1040+
const overwriteMessages = messages.filter(arg => arg[0].command === 'overwritePlaylistInfoInDB');
1041+
checkPlaylistsUploadedToDB(overwriteMessages, input);
1042+
10111043
expect(numDeletedVideosBefore).to.be.greaterThan(numDeletedVideosAfter);
10121044
break;
10131045
case 6:
10141046
expect(commands).to.contain('getAPIKey');
10151047
if (numDeletedVideosBefore > numDeletedVideosAfter) {
10161048
expect(commands).to.contain('overwritePlaylistInfoInDB');
1049+
const overwriteMessages = messages.filter(arg => arg[0].command === 'overwritePlaylistInfoInDB');
1050+
checkPlaylistsUploadedToDB(overwriteMessages, input);
10171051
} else {
10181052
expect(commands).to.contain('updatePlaylistInfoInDB');
1053+
const updateMessages = messages.filter(arg => arg[0].command === 'updatePlaylistInfoInDB');
1054+
checkPlaylistsUploadedToDB(updateMessages, input);
10191055
}
10201056
break;
10211057
default:
@@ -1027,7 +1063,8 @@ describe('shuffleVideo', function () {
10271063
await chooseRandomVideo(input.channelId, false, domElement);
10281064
const playlistInfoAfter = await getKeyFromLocalStorage(input.playlistId);
10291065

1030-
const commands = chrome.runtime.sendMessage.args.map(arg => arg[0].command);
1066+
const messages = chrome.runtime.sendMessage.args;
1067+
const commands = messages.map(arg => arg[0].command);
10311068

10321069
expect(commands).to.contain('connectionTest');
10331070
expect(commands).to.contain('getPlaylistFromDB');
@@ -1041,6 +1078,8 @@ describe('shuffleVideo', function () {
10411078
expect(chrome.runtime.sendMessage.callCount).to.be(6);
10421079
expect(commands).to.contain('getAPIKey');
10431080
expect(commands).to.contain('updatePlaylistInfoInDB');
1081+
const updateMessages = messages.filter(arg => arg[0].command === 'updatePlaylistInfoInDB');
1082+
checkPlaylistsUploadedToDB(updateMessages, input);
10441083
} else {
10451084
expect(chrome.runtime.sendMessage.callCount).to.be(4);
10461085
}
@@ -1053,7 +1092,8 @@ describe('shuffleVideo', function () {
10531092
it('should only fetch data from the database', async function () {
10541093
await chooseRandomVideo(input.channelId, false, domElement);
10551094

1056-
const commands = chrome.runtime.sendMessage.args.map(arg => arg[0].command);
1095+
const messages = chrome.runtime.sendMessage.args;
1096+
const commands = messages.map(arg => arg[0].command);
10571097

10581098
// 4 because we only need to fetch from the DB
10591099
expect(chrome.runtime.sendMessage.callCount).to.be(4);
@@ -1068,7 +1108,8 @@ describe('shuffleVideo', function () {
10681108
it('should update the database', async function () {
10691109
await chooseRandomVideo(input.channelId, false, domElement);
10701110

1071-
const commands = chrome.runtime.sendMessage.args.map(arg => arg[0].command);
1111+
const messages = chrome.runtime.sendMessage.args;
1112+
const commands = messages.map(arg => arg[0].command);
10721113

10731114
// 6 because we need to fetch from the DB, fetch from the YT API, and update the DB
10741115
expect(chrome.runtime.sendMessage.callCount).to.be(6);
@@ -1079,6 +1120,8 @@ describe('shuffleVideo', function () {
10791120
expect(commands).to.contain('getCurrentTabId');
10801121
expect(commands).to.contain('getAPIKey');
10811122
expect(commands).to.contain('updatePlaylistInfoInDB');
1123+
const updateMessages = messages.filter(arg => arg[0].command === 'updatePlaylistInfoInDB');
1124+
checkPlaylistsUploadedToDB(updateMessages, input);
10821125
});
10831126
}
10841127
});

0 commit comments

Comments
 (0)