basic navidrome implementation

This commit is contained in:
simojenki
2021-03-01 17:28:48 +11:00
parent 3b350c4402
commit 007db24713
17 changed files with 305 additions and 105 deletions

View File

@@ -1,9 +1,9 @@
import { SonosDevice } from "@svrooij/sonos/lib";
import { ArtistWithAlbums } from "in_memory_music_service";
import { v4 as uuid } from "uuid";
import { Credentials } from "../src/smapi";
import { Service, Device } from "../src/sonos";
import { Album, Artist } from "../src/music_service";
const randomInt = (max: number) => Math.floor(Math.random() * max);
const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`;
@@ -68,6 +68,10 @@ export function someCredentials(token: string): Credentials {
}
}
export type ArtistWithAlbums = Artist & {
albums: Album[];
};
export const BOB_MARLEY: ArtistWithAlbums = {
id: uuid(),
name: "Bob Marley",

12
tests/encryption.test.ts Normal file
View File

@@ -0,0 +1,12 @@
import encryption from '../src/encryption';
describe("encrypt", () => {
const e = encryption();
it("can encrypt and decrypt", () => {
const value = "bobs your uncle"
const hash = e.encrypt(value)
expect(hash.encryptedData).not.toEqual(value);
expect(e.decrypt(hash)).toEqual(value);
});
})

View File

@@ -1,20 +1,18 @@
import {
InMemoryMusicService,
} from "./in_memory_music_service";
import { InMemoryMusicService } from "./in_memory_music_service";
import { AuthSuccess, MusicLibrary } from "../src/music_service";
import { v4 as uuid } from "uuid";
import { BOB_MARLEY, MADONNA, BLONDIE } from './builders'
import { BOB_MARLEY, MADONNA, BLONDIE } from "./builders";
describe("InMemoryMusicService", () => {
const service = new InMemoryMusicService();
describe("generateToken", () => {
it("should be able to generate a token and then use it to log in", () => {
it("should be able to generate a token and then use it to log in", async () => {
const credentials = { username: "bob", password: "smith" };
service.hasUser(credentials);
const token = service.generateToken(credentials) as AuthSuccess;
const token = (await service.generateToken(credentials)) as AuthSuccess;
expect(token.userId).toEqual(credentials.username);
expect(token.nickname).toEqual(credentials.username);
@@ -24,35 +22,31 @@ describe("InMemoryMusicService", () => {
expect(musicLibrary).toBeDefined();
});
it("should fail with an exception if an invalid token is used", () => {
it.only("should fail with an exception if an invalid token is used", async () => {
const credentials = { username: "bob", password: "smith" };
service.hasUser(credentials);
const token = service.generateToken(credentials) as AuthSuccess;
const token = (await service.generateToken(credentials)) as AuthSuccess;
service.clear();
expect(service.login(token.authToken)).toEqual({
message: "Invalid auth token",
});
return expect(service.login(token.authToken)).rejects.toEqual("Invalid auth token");
});
});
describe("Music Library", () => {
const user = { username: "user100", password: "password100" };
let musicLibrary: MusicLibrary;
beforeEach(() => {
beforeEach(async () => {
service.clear();
service.hasArtists(BOB_MARLEY, MADONNA, BLONDIE);
service.hasUser(user);
const token = service.generateToken(user) as AuthSuccess;
musicLibrary = service.login(token.authToken) as MusicLibrary;
const token = (await service.generateToken(user)) as AuthSuccess;
musicLibrary = (await service.login(token.authToken)) as MusicLibrary;
});
describe("artists", () => {

View File

@@ -1,18 +1,16 @@
import { option as O } from "fp-ts";
import { pipe } from "fp-ts/lib/function";
import { ArtistWithAlbums } from "./builders";
import {
MusicService,
Credentials,
AuthSuccess,
AuthFailure,
Artist,
Album,
MusicLibrary,
} from "../src/music_service";
export type ArtistWithAlbums = Artist & {
albums: Album[];
};
const artistWithAlbumsToArtist = (it: ArtistWithAlbums): Artist => ({
id: it.id,
@@ -36,30 +34,26 @@ export class InMemoryMusicService implements MusicService {
generateToken({
username,
password,
}: Credentials): AuthSuccess | AuthFailure {
}: Credentials): Promise<AuthSuccess | AuthFailure> {
if (
username != undefined &&
password != undefined &&
this.users[username] == password
) {
return {
return Promise.resolve({
authToken: JSON.stringify({ username, password }),
userId: username,
nickname: username,
};
});
} else {
return { message: `Invalid user:${username}` };
return Promise.resolve({ message: `Invalid user:${username}` });
}
}
login(token: string): MusicLibrary | AuthFailure {
login(token: string): Promise<MusicLibrary> {
const credentials = JSON.parse(token) as Credentials;
if (this.users[credentials.username] != credentials.password) {
return {
message: "Invalid auth token",
};
}
return {
if (this.users[credentials.username] != credentials.password) return Promise.reject("Invalid auth token")
return Promise.resolve({
artists: () => this.artists.map(artistWithAlbumsToArtist),
artist: (id: string) =>
pipe(
@@ -90,7 +84,7 @@ export class InMemoryMusicService implements MusicService {
.flatMap((it) => it.albums)
.slice(i0, i1);
},
};
});
}
hasUser(credentials: Credentials) {

75
tests/navidrome.test.ts Normal file
View File

@@ -0,0 +1,75 @@
import { Md5 } from "ts-md5/dist/md5";
import { Navidrome, t } from "../src/navidrome";
import encryption from "../src/encryption";
import axios from "axios";
jest.mock("axios");
import randomString from "../src/random_string";
jest.mock("../src/random_string");
describe("t", () => {
it("should be an md5 of the password and the salt", () => {
const p = "password123";
const s = "saltydog";
expect(t(p, s)).toEqual(Md5.hashStr(`${p}${s}`));
});
});
describe("navidrome", () => {
const url = "http://127.0.0.22:4567";
const username = "user1";
const password = "pass1";
const salt = "saltysalty";
const navidrome = new Navidrome(url, encryption());
const mockedRandomString = (randomString as unknown) as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
axios.get = jest.fn();
mockedRandomString.mockReturnValue(salt);
});
describe("generateToken", () => {
describe("when the credentials are valid", () => {
it("should be able to generate a token and then login using it", async () => {
(axios.get as jest.Mock).mockResolvedValue({
status: 200,
data: `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
</subsonic-response>`,
});
const token = await navidrome.generateToken({ username, password });
expect(token.authToken).toBeDefined();
expect(token.nickname).toEqual(username);
expect(token.userId).toEqual(username);
expect(axios.get).toHaveBeenCalledWith(
`${url}/rest/ping.view?u=${username}&t=${t(
password,
salt
)}&s=${salt}&v=1.16.1.0&c=bonob`
);
});
});
describe("when the credentials are not valid", () => {
it("should be able to generate a token and then login using it", async () => {
(axios.get as jest.Mock).mockResolvedValue({
status: 200,
data: `<subsonic-response xmlns="http://subsonic.org/restapi" status="failed" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
<error code="40" message="Wrong username or password"></error>
</subsonic-response>`,
});
return expect(navidrome.generateToken({ username, password })).rejects.toMatch("Wrong username or password");
});
});
});
});

View File

@@ -0,0 +1,16 @@
import randomString from "../src/random_string";
describe('randomString', () => {
it('should produce different strings...', () => {
const s1 = randomString()
const s2 = randomString()
const s3 = randomString()
const s4 = randomString()
expect(s1.length).toEqual(64)
expect(s1).not.toEqual(s2);
expect(s1).not.toEqual(s3);
expect(s1).not.toEqual(s4);
});
});

View File

@@ -289,7 +289,7 @@ describe("api", () => {
const username = "userThatGetsDeleted";
const password = "password1";
musicService.hasUser({ username, password });
const token = musicService.generateToken({
const token = await musicService.generateToken({
username,
password,
}) as AuthSuccess;
@@ -321,7 +321,7 @@ describe("api", () => {
beforeEach(async () => {
musicService.hasUser({ username, password });
token = musicService.generateToken({
token = await musicService.generateToken({
username,
password,
}) as AuthSuccess;