Skip to content

Commit

Permalink
LB-806: Importer connection (#1270)
Browse files Browse the repository at this point in the history
* Importer connection test

* Error message + tests

* Correction of LastFM importer error message test

* Fix linting issues

Co-authored-by: Jason Dao <jasondaok@example.com>
Co-authored-by: Monkey Do <contact@monkeydo.digital>
  • Loading branch information
3 people committed Feb 10, 2021
1 parent ff733af commit 5d22181
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 37 deletions.
75 changes: 71 additions & 4 deletions listenbrainz/webserver/static/js/src/LastFMImporter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ describe("LastFmImporter Page", () => {

it("should properly convert latest imported timestamp to string", () => {
// Check getlastImportedString() and formatting
const testDate = Number(page.recenttracks.track[0].date.uts);
const lastImportedDate = new Date(testDate * 1000);
const data = LastFmImporter.encodeScrobbles(page);
const lastImportedDate = new Date(data[0].listened_at * 1000);
const msg = lastImportedDate.toLocaleString("en-US", {
month: "short",
day: "2-digit",
Expand All @@ -325,7 +325,74 @@ describe("LastFmImporter Page", () => {
hour12: true,
});

expect(LastFmImporter.getlastImportedString(testDate)).toMatch(msg);
expect(LastFmImporter.getlastImportedString(testDate)).not.toHaveLength(0);
expect(LastFmImporter.getlastImportedString(data[0])).toMatch(msg);
expect(LastFmImporter.getlastImportedString(data[0])).not.toHaveLength(0);
});
});

describe("importLoop", () => {
beforeEach(() => {
const wrapper = shallow<LastFmImporter>(<LastFmImporter {...props} />);
instance = wrapper.instance();
instance.setState({ lastfmUsername: "dummyUser" });
// needed for startImport
instance.APIService.getLatestImport = jest.fn().mockImplementation(() => {
return Promise.resolve(0);
});

// Mock function for fetch
window.fetch = jest.fn().mockImplementation(() => {
return Promise.resolve({
ok: true,
json: () => Promise.resolve(getInfo),
});
});
});

it("should not contain any uncaught exceptions", async () => {
instance.getPage = jest.fn().mockImplementation(() => {
return null;
});

let error = null;
try {
await instance.importLoop();
} catch (e) {
error = e;
}
expect(error).toBeNull();
});

it("should show success message on import completion", async () => {
// Mock function for successful importLoop
instance.importLoop = jest.fn().mockImplementation(async () => {
return Promise.resolve({
ok: true,
});
});

await expect(instance.startImport()).resolves.toBe(null);
// verify message is success message
expect(instance.state.msg?.props.children).toContain("Import finished");
// verify message isn't failure message
expect(instance.state.msg?.props.children).not.toContain(
"Something went wrong"
);
});

it("should show error message on unhandled exception / network error", async () => {
const errorMsg = "Testing: something went wrong !!!";
// Mock function for failed importLoop
instance.importLoop = jest.fn().mockImplementation(async () => {
const error = new Error();
// Changing the error message to make sure it gets reflected in the modal.
error.message = errorMsg;
throw error;
});

// startImport shouldn't throw error
await expect(instance.startImport()).resolves.toBe(null);
// verify message is failure message
expect(instance.state.msg?.props.children).toContain(errorMsg);
});
});
101 changes: 68 additions & 33 deletions listenbrainz/webserver/static/js/src/LastFMImporter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as ReactDOM from "react-dom";
import * as React from "react";
import { faSpinner, faCheck } from "@fortawesome/free-solid-svg-icons";
import { faSpinner, faCheck, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import APIService from "./APIService";
Expand Down Expand Up @@ -100,19 +100,6 @@ export default class LastFmImporter extends React.Component<
this.userToken = props.user.auth_token || "";
}

static getlastImportedString(listenedAt: number) {
// Retrieve first track's timestamp from payload and convert it into string for display
const lastImportedDate = new Date(listenedAt * 1000);
return lastImportedDate.toLocaleString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
});
}

async getTotalNumberOfScrobbles() {
/*
* Get the total play count reported by Last.FM for user
Expand Down Expand Up @@ -171,7 +158,10 @@ export default class LastFmImporter extends React.Component<
const { lastfmUsername } = this.state;

const retry = (reason: string) => {
// console.warn(`${reason} while fetching last.fm page=${page}, retrying in 3s`);
// eslint-disable-next-line no-console
console.warn(
`${reason} while fetching last.fm page=${page}, retrying in 3s`
);
setTimeout(() => this.getPage(page), 3000);
};

Expand Down Expand Up @@ -207,11 +197,24 @@ export default class LastFmImporter extends React.Component<
}
} catch {
// Retry if there is a network error
retry("Error");
retry("Network error");
}
return null;
}

static getlastImportedString(listen: Listen) {
// Retrieve first track's timestamp from payload and convert it into string for display
const lastImportedDate = new Date(listen.listened_at * 1000);
return lastImportedDate.toLocaleString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
});
}

getRateLimitDelay() {
/* Get the amount of time we should wait according to LB rate limits before making a request to LB */
let delay = 0;
Expand Down Expand Up @@ -269,29 +272,21 @@ export default class LastFmImporter extends React.Component<
this.updateRateLimitParameters(response);
}

async startImport() {
this.updateModalAction(<p>Your import from Last.fm is starting!</p>, false);
this.latestImportTime = await this.APIService.getLatestImport(
this.userName
);
this.incrementalImport = this.latestImportTime > 0;
this.playCount = await this.getTotalNumberOfScrobbles();
this.totalPages = await this.getNumberOfPages();
this.page = this.totalPages; // Start from the last page so that oldest scrobbles are imported first

async importLoop() {
while (this.page > 0) {
// Fixing no-await-in-loop will require significant changes to the code, ignoring for now
this.lastImportedString = "...";
const payload = await this.getPage(this.page); // eslint-disable-line
if (payload) {
// Submit only if response is valid
this.submitPage(payload);
this.lastImportedString = LastFmImporter.getlastImportedString(
payload[0]
);
}

this.page -= 1;
this.numCompleted += 1;
this.lastImportedString = LastFmImporter.getlastImportedString(
payload[0].listened_at
);

// Update message
const msg = (
Expand All @@ -316,8 +311,47 @@ export default class LastFmImporter extends React.Component<
);
this.setState({ msg });
}
}

// Update latest import time on LB server
async startImport() {
this.updateModalAction(<p>Your import from Last.fm is starting!</p>, false);
this.latestImportTime = await this.APIService.getLatestImport(
this.userName
);
this.incrementalImport = this.latestImportTime > 0;
this.playCount = await this.getTotalNumberOfScrobbles();
this.totalPages = await this.getNumberOfPages();
this.page = this.totalPages; // Start from the last page so that oldest scrobbles are imported first

let finalMsg: JSX.Element;
const { profileUrl } = this.props;

try {
await this.importLoop(); // import pages
} catch (err) {
// import failed, show final message on unhandled exception / unrecoverable network error
finalMsg = (
<p>
<FontAwesomeIcon icon={faTimes as IconProp} /> Import failed due to a
network error, please retry.
<br />
Message: &quot;{err.message}&quot;
<br />
<br />
<span style={{ fontSize: `${10}pt` }}>
<a href={`${profileUrl}`}>
Close and go to your ListenBrainz profile
</a>
</span>
</p>
);
// eslint-disable-next-line no-console
console.debug(err);
this.setState({ canClose: true, msg: finalMsg });
return Promise.resolve(null);
}

// import was successful
try {
this.maxTimestampForImport = Math.max(
Number(this.maxTimestampForImport),
Expand All @@ -338,10 +372,10 @@ export default class LastFmImporter extends React.Component<
3000
);
}
const { profileUrl } = this.props;
const finalMsg = (
finalMsg = (
<p>
<FontAwesomeIcon icon={faCheck as IconProp} /> Import finished
<FontAwesomeIcon icon={faCheck as IconProp} />
Import finished
<br />
<span style={{ fontSize: `${8}pt` }}>
Successfully submitted {this.countReceived} listens to ListenBrainz
Expand Down Expand Up @@ -376,6 +410,7 @@ export default class LastFmImporter extends React.Component<
</p>
);
this.setState({ canClose: true, msg: finalMsg });
return Promise.resolve(null);
}

updateRateLimitParameters(response: Response) {
Expand Down

0 comments on commit 5d22181

Please sign in to comment.