From ebf846c15407ff08ae6f56e829649a0677017c44 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Thu, 5 Sep 2019 12:08:21 +0200 Subject: [PATCH] Add mocking for peerJS and first test --- README.md | 2 + package.json | 1 + src/network/client.ts | 17 +- src/network/index.ts | 5 + src/network/local.ts | 4 +- src/network/peer.ts | 8 +- src/network/server.ts | 18 +- src/network/types.ts | 2 +- src/testing/LocalDataConnection.ts | 219 +++++++++++++++++++++++++ src/testing/LocalPeer.ts | 88 ++++++++++ {tests => src/tests}/unit/.eslintrc.js | 0 src/tests/unit/server.spec.ts | 23 +++ yarn.lock | 5 + 13 files changed, 375 insertions(+), 17 deletions(-) create mode 100644 src/network/index.ts create mode 100644 src/testing/LocalDataConnection.ts create mode 100644 src/testing/LocalPeer.ts rename {tests => src/tests}/unit/.eslintrc.js (100%) create mode 100644 src/tests/unit/server.spec.ts diff --git a/README.md b/README.md index 627e18d..36c499a 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,5 @@ Work in progress name, work in progress game ## License Code is ISC, Assets "depends", some stuff is taken from FreeSound and DeviantArt so your best bet is to just praise the copyright gods. + +PeerJS mocking is based on works by Rolf Erik Lekang, which is licensed under MIT. Check [peerjs-mock](https://github.com/relekang/peerjs-mock) for more details. diff --git a/package.json b/package.json index 78af567..26304fa 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "buefy": "^0.8.2", "core-js": "^2.6.5", "dexie": "^2.0.4", + "eventemitter3": "^4.0.0", "peerjs": "^1.0.4", "register-service-worker": "^1.6.2", "vue": "^2.6.10", diff --git a/src/network/client.ts b/src/network/client.ts index 50ace8a..7019928 100644 --- a/src/network/client.ts +++ b/src/network/client.ts @@ -1,21 +1,26 @@ import NetworkPeer from "./peer"; -import { DataConnection } from "peerjs"; +import Peer, { DataConnection } from "peerjs"; import { PeerMetadata, NetworkMessage } from "./types"; -export default class PeerClient extends NetworkPeer { +export class PeerClient extends NetworkPeer { private connection: DataConnection; - public constructor(peerid: string, metadata: PeerMetadata) { - super(); + public constructor( + peerid: string, + metadata: PeerMetadata, + customPeer?: Peer + ) { + super(customPeer); this.connection = this.peer.connect(peerid, { - label: "server", metadata, reliable: true }); this.connection.on("open", () => { console.info("Connected to server"); }); - this.connection.on("data", this._received); + this.connection.on("data", this._received.bind(this)); } private _received(data: NetworkMessage) {} } + +export default PeerClient; diff --git a/src/network/index.ts b/src/network/index.ts new file mode 100644 index 0000000..da0b10c --- /dev/null +++ b/src/network/index.ts @@ -0,0 +1,5 @@ +export * from "./client"; +export * from "./local"; +export * from "./peer"; +export * from "./server"; +export * from "./types"; diff --git a/src/network/local.ts b/src/network/local.ts index 980a054..6f5d4dc 100644 --- a/src/network/local.ts +++ b/src/network/local.ts @@ -1,6 +1,6 @@ import { PeerMetadata, NetworkMessage } from "./types"; -export default class LocalClient { +export class LocalClient { public metadata: PeerMetadata; public receiver!: (data: NetworkMessage) => void; @@ -16,3 +16,5 @@ export default class LocalClient { this.receiver(data); } } + +export default LocalClient; diff --git a/src/network/peer.ts b/src/network/peer.ts index 7658fca..d8402b9 100644 --- a/src/network/peer.ts +++ b/src/network/peer.ts @@ -1,10 +1,10 @@ import Peer, { DataConnection } from "peerjs"; import { NetworkMessage } from "./types"; -export default class NetworkPeer { +export class NetworkPeer { protected peer: Peer; - protected constructor() { - this.peer = new Peer(); + protected constructor(peer?: Peer) { + this.peer = peer ? peer : new Peer(); this.peer.on("open", function(id) { console.info("Peer ID assigned: %s", id); }); @@ -15,3 +15,5 @@ export default class NetworkPeer { conn.send(data); } } + +export default NetworkPeer; diff --git a/src/network/server.ts b/src/network/server.ts index f49d70a..1867bd0 100644 --- a/src/network/server.ts +++ b/src/network/server.ts @@ -1,5 +1,5 @@ import NetworkPeer from "./peer"; -import { DataConnection } from "peerjs"; +import Peer, { DataConnection } from "peerjs"; import { RoomInfo, PasswordRequest, @@ -33,11 +33,15 @@ function nextName(name: string): string { return name.substr(0, i) + (Number(name.slice(i)) + 1); } -export default class PeerServer extends NetworkPeer { +export class PeerServer extends NetworkPeer { private room: Room; - public constructor(roomInfo: RoomInfo, local: LocalClient) { - super(); + public constructor( + roomInfo: RoomInfo, + local: LocalClient, + customPeer?: Peer + ) { + super(customPeer); let players: Record = {}; // Add local player to server @@ -52,7 +56,7 @@ export default class PeerServer extends NetworkPeer { info: roomInfo, players }; - this.peer.on("connection", this._connection); + this.peer.on("connection", this._connection.bind(this)); } private _connection(conn: DataConnection) { @@ -105,7 +109,7 @@ export default class PeerServer extends NetworkPeer { }; this.send(conn, { kind: "password-req" }); - conn.on("data", checkPasswordResponse); + conn.on("data", checkPasswordResponse.bind(this)); return; } @@ -184,3 +188,5 @@ export default class PeerServer extends NetworkPeer { } } } + +export default PeerServer; diff --git a/src/network/types.ts b/src/network/types.ts index a84f039..d5b2033 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -27,7 +27,7 @@ export interface Room { export interface RoomInfo { max_players: number; password: string; - game: GameInfo; + game?: GameInfo; } type GameInfo = MatchInfo | DraftInfo; diff --git a/src/testing/LocalDataConnection.ts b/src/testing/LocalDataConnection.ts new file mode 100644 index 0000000..ee6e83f --- /dev/null +++ b/src/testing/LocalDataConnection.ts @@ -0,0 +1,219 @@ +import EventEmitter from "eventemitter3"; +import LocalPeer from "./LocalPeer"; +import { PeerConnectOption } from "peerjs"; + +export default class LocalDataConnection extends EventEmitter { + public options: PeerConnectOption; + public peer: string; + public provider: LocalPeer; + public open: boolean = false; + private _connection?: LocalDataConnection; + public id: string; + public label: string; + + public constructor( + peer: string, + provider: LocalPeer, + options?: PeerConnectOption + ) { + super(); + this.options = { + serialization: "binary", + reliable: false + }; + if (options) { + this.options = Object.assign(this.options, options); + } + this.id = "fake_" + Math.random().toString(32); + this.label = this.options.label ? this.options.label : this.id; + this.provider = provider; + this.peer = peer; + } + + public send(message: any) { + if (!this.open) { + this.emit( + "error", + new Error( + "Connection is not open. You should listen for the `open` event before sending messages." + ) + ); + } + const connection = this._connection; + if (connection) { + connection.receive(message); + } + } + + public receive(message: any) { + this.emit("data", message); + } + + public close() { + if (!this.open) { + return; + } + this.open = false; + this.emit("close"); + } + + public _setRemote(connection: LocalDataConnection) { + this._connection = connection; + this._connection.on("open", () => { + if (!this.open) { + this.open = true; + this.emit("open"); + } + }); + } + + public _configureDataChannel( + mocks: Record, + options?: PeerConnectOption + ) { + if (!this._connection && this.peer in mocks) { + this._setRemote( + mocks[this.peer]._negotiate(this.provider.id, this, options) + ); + this.open = true; + this.emit("open"); + } else { + throw new Error(`Peer(${this.peer}) not found`); + } + } + + public get metadata() { + return this.options.metadata; + } + + public get reliable() { + return this.options.reliable ? this.options.reliable : false; + } + + public get serialization() { + return this.options.serialization ? this.options.serialization : "binary"; + } + + public get peerConnection() { + return this._connection; + } + + /* + UNIMPLEMENTED STUFF + */ + + public dataChannel: unknown; + public bufferSize: unknown; + public type: unknown; + public canTrickleIceCandidates: unknown; + public connectionState: unknown; + public currentLocalDescription: unknown; + public currentRemoteDescription: unknown; + public iceConnectionState: unknown; + public iceGatheringState: unknown; + public idpErrorInfo: unknown; + public idpLoginUrl: unknown; + public localDescription: unknown; + public onconnectionstatechange: unknown; + public ondatachannel: unknown; + public onicecandidate: unknown; + public onicecandidateerror: unknown; + public oniceconnectionstatechange: unknown; + public onicegatheringstatechange: unknown; + public onnegotiationneeded: unknown; + public onsignalingstatechange: unknown; + public onstatsended: unknown; + public ontrack: unknown; + public peerIdentity: unknown; + public pendingLocalDescription: unknown; + public pendingRemoteDescription: unknown; + public remoteDescription: unknown; + public sctp: unknown; + public signalingState: unknown; + + public addIceCandidate( + candidate: RTCIceCandidateInit | RTCIceCandidate + ): Promise { + throw new Error("not implemented"); + } + public addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): unknown { + throw new Error("not implemented"); + } + public addTransceiver( + trackOrKind: MediaStreamTrack | string, + init?: RTCRtpTransceiverInit + ): unknown { + throw new Error("not implemented"); + } + public createAnswer(options?: RTCOfferOptions): Promise { + throw new Error("not implemented"); + } + public createDataChannel( + label: string, + dataChannelDict?: RTCDataChannelInit + ): unknown { + throw new Error("not implemented"); + } + public createOffer(options?: RTCOfferOptions): Promise { + throw new Error("not implemented"); + } + public getConfiguration(): unknown { + throw new Error("not implemented"); + } + public getIdentityAssertion(): Promise { + throw new Error("not implemented"); + } + public getReceivers(): unknown[] { + throw new Error("not implemented"); + } + public getSenders(): unknown[] { + throw new Error("not implemented"); + } + public getStats(selector?: MediaStreamTrack | null): Promise { + throw new Error("not implemented"); + } + public getTransceivers(): unknown[] { + throw new Error("not implemented"); + } + public removeTrack(sender: RTCRtpSender): void { + throw new Error("not implemented"); + } + public setConfiguration(configuration: RTCConfiguration): void { + throw new Error("not implemented"); + } + public setIdentityProvider( + provider: string, + options?: RTCIdentityProviderOptions + ) { + throw new Error("not implemented"); + } + public setLocalDescription( + description: RTCSessionDescriptionInit + ): Promise { + throw new Error("not implemented"); + } + public setRemoteDescription( + description: RTCSessionDescriptionInit + ): Promise { + throw new Error("not implemented"); + } + + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + throw new Error("not implemented"); + } + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ): void { + throw new Error("not implemented"); + } + + dispatchEvent(event: Event): boolean { + throw new Error("not implemented"); + } +} diff --git a/src/testing/LocalPeer.ts b/src/testing/LocalPeer.ts new file mode 100644 index 0000000..661fec1 --- /dev/null +++ b/src/testing/LocalPeer.ts @@ -0,0 +1,88 @@ +import EventEmitter from "eventemitter3"; +import Peer, { PeerConnectOption, PeerJSOption } from "peerjs"; +import LocalDataConnection from "./LocalDataConnection"; + +let mocks: Record = {}; + +export default class LocalPeer extends EventEmitter { + public connections: Record; + public id: string; + public options: PeerJSOption; + public disconnected: boolean; + public destroyed: boolean; + public prototype: unknown; + + public constructor(id: string, options?: PeerJSOption) { + super(); + this.id = id; + this.options = options ? options : {}; + this.connections = {}; + this.disconnected = false; + this.destroyed = false; + mocks[id] = this; + } + + public connect(id: string, options?: PeerConnectOption): LocalDataConnection { + let connection = new LocalDataConnection(id, this, options); + connection._configureDataChannel(mocks, options); + this._addConnection(id, connection); + return connection; + } + + public disconnect() { + this.disconnected = true; + this.emit("disconnected", this.id); + this.id = ""; + } + + public destroy() { + this.destroyed = true; + this.emit("destroyed", this.id); + this.disconnect(); + } + + private _addConnection(peer: string, connection: LocalDataConnection) { + if (!this.connections[peer]) { + this.connections[peer] = []; + } + this.connections[peer].push(connection); + } + + public _negotiate( + peer: string, + remoteConnection: LocalDataConnection, + options?: PeerConnectOption + ) { + let localConnection = new LocalDataConnection(peer, this, options); + localConnection._setRemote(remoteConnection); + this._addConnection(peer, localConnection); + this.emit("connection", localConnection); + return localConnection; + } + + public call( + id: string, + stream: MediaStream, + options?: Peer.CallOption + ): Peer.MediaConnection { + throw new Error("unimplemented"); + } + + public reconnect() {} + + public getConnection( + peerId: string, + connectionId: string + ): LocalDataConnection | null { + if (peerId in mocks) { + let map = mocks[peerId].connections; + if (connectionId in map) { + return map[connectionId][0]; + } + return null; + } + return null; + } + + public listAllPeers() {} +} diff --git a/tests/unit/.eslintrc.js b/src/tests/unit/.eslintrc.js similarity index 100% rename from tests/unit/.eslintrc.js rename to src/tests/unit/.eslintrc.js diff --git a/src/tests/unit/server.spec.ts b/src/tests/unit/server.spec.ts new file mode 100644 index 0000000..581aa91 --- /dev/null +++ b/src/tests/unit/server.spec.ts @@ -0,0 +1,23 @@ +import LocalPeer from "@/testing/LocalPeer"; +import Peer from "peerjs"; +import { PeerServer, LocalClient, PeerClient } from "@/network"; + +describe("network/PeerServer", () => { + test("Create server and client", () => { + const serverPeer = new LocalPeer("test-server", {}); + const clientPeer = new LocalPeer("test-client", {}); + const serverPlayer = new LocalClient({ + name: "server-client" + }); + const roomInfo = { + max_players: 3, + password: "" + }; + const server = new PeerServer(roomInfo, serverPlayer, serverPeer as Peer); + const client = new PeerClient( + "test-server", + { name: "test" }, + clientPeer as Peer + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index cf86421..6cdb382 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3889,6 +3889,11 @@ eventemitter3@^3.0.0, eventemitter3@^3.1.2: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +eventemitter3@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" + integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + events@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88"