<commit_before>const { Intent } = require("../..");
const log = require("../log");

describe("Intent", function() {
    let intent, client, botClient;
    const userId = "@alice:bar";
    const botUserId = "@bot:user";
    const roomId = "!foo:bar";
    const alreadyRegistered = {
        registered: true
    };

    beforeEach(
    /** @this */
    function() {
        log.beforeEach(this);
        const clientFields = [
            "credentials", "joinRoom", "invite", "leave", "ban", "unban",
            "kick", "getStateEvent", "setPowerLevel", "sendTyping", "sendEvent",
            "sendStateEvent", "setDisplayName", "setAvatarUrl",
        ];
        client = jasmine.createSpyObj("client", clientFields);
        client.credentials.userId = userId;
        botClient = jasmine.createSpyObj("botClient", clientFields);
        botClient.credentials.userId = botUserId;
        intent = new Intent(client, botClient, alreadyRegistered);
    });

    describe("join", function() {

        it("should /join/$ROOMID if it doesn't know it is already joined",
        function() {
            client.joinRoom.and.callFake(() => Promise.resolve({room_id: roomId}));
            return intent.join(roomId).then(function(resultRoomId) {
                expect(client.joinRoom).toHaveBeenCalledWith(
                    roomId, { syncRoom: false }
                );
                expect(resultRoomId).toBe(roomId);
            });
        });

        it("should no-op if it knows it is already joined", function() {
            intent.onEvent({
                event_id: "test",
                type: "m.room.member",
                state_key: userId,
                room_id: roomId,
                content: {
                    membership: "join"
                }
            });
            return intent.join(roomId).then(function(resultRoomId) {
                expect(resultRoomId).toBe(roomId);
                expect(client.joinRoom).not.toHaveBeenCalled();
            });
        });

        it("should fail if the join returned an error other than forbidden",
        function() {
            client.joinRoom.and.callFake(() => Promise.reject({
                errcode: "M_YOU_ARE_A_FISH",
                error: "you're a fish"
            }));
            return intent.join(roomId).catch(function() {
                expect(client.joinRoom).toHaveBeenCalled();
            });
        });

        describe("client join failed", function() {

            it("should make the bot invite then the client join", function() {
                client.joinRoom.and.callFake(function() {
                    if (botClient.invite.calls.count() === 0) {
                        return Promise.reject({
                            errcode: "M_FORBIDDEN",
                            error: "Join first"
                        });
                    }
                    return Promise.resolve({room_id: roomId});
                });
                botClient.invite.and.callFake(() => Promise.resolve({}));

                return intent.join(roomId).then(function(resultRoomId) {
                    expect(client.joinRoom).toHaveBeenCalledWith(
                        roomId, { syncRoom: false }
                    );
                    expect(botClient.invite).toHaveBeenCalledWith(roomId, userId);
                    expect(resultRoomId).toBe(roomId);
                });
            });

            describe("bot invite failed", function() {
                it("should make the bot join then invite then the client join",
                function() {
                    client.joinRoom.and.callFake(function() {
                        if (botClient.invite.calls.count() === 0) {
                            return Promise.reject({
                                errcode: "M_FORBIDDEN",
                                error: "Join first"
                            });
                        }
                        return Promise.resolve({});
                    });
                    botClient.invite.and.callFake(function() {
                        if (botClient.joinRoom.calls.count() === 0) {
                            return Promise.reject({
                                errcode: "M_FORBIDDEN",
                                error: "Join first"
                            });
                        }
                        return Promise.resolve({});
                    });
                    botClient.joinRoom.and.callFake(() => Promise.resolve({room_id: roomId}));

                    return intent.join(roomId).then(function(resultRoomId) {
                        expect(client.joinRoom).toHaveBeenCalledWith(
                            roomId, { syncRoom: false }
                        );
                        expect(botClient.invite).toHaveBeenCalledWith(roomId, userId);
                        expect(botClient.joinRoom).toHaveBeenCalledWith(
                            roomId, { syncRoom: false }
                        );
                        expect(resultRoomId).toBe(roomId);

                    });
                });

                it("should give up if the bot cannot join the room", function() {
                    client.joinRoom.and.callFake(() => Promise.reject({
                        errcode: "M_FORBIDDEN",
                        error: "Join first"
                    }));
                    botClient.invite.and.callFake(() => Promise.reject({
                        errcode: "M_FORBIDDEN",
                        error: "No invites kthx"
                    }));
                    botClient.joinRoom.and.callFake(() => Promise.reject({
                        errcode: "M_FORBIDDEN",
                        error: "No bots allowed!"
                    }));

                    return intent.join(roomId).catch(function() {
                        expect(client.joinRoom).toHaveBeenCalledWith(
                            roomId, { syncRoom: false }
                        );
                        expect(botClient.invite).toHaveBeenCalledWith(roomId, userId);
                        expect(botClient.joinRoom).toHaveBeenCalledWith(
                            roomId, { syncRoom: false }
                        );
                    });
                });
            });
        });
    });

    describe("sending state events", function() {
        let validPowerLevels, invalidPowerLevels;

        beforeEach(() => {
            // not interested in joins, so no-op them.
            intent.onEvent({
                event_id: "test",
                type: "m.room.member",
                state_key: userId,
                room_id: roomId,
                content: {
                    membership: "join"
                }
            });

            const basePowerLevelEvent = {
                content: {
                    "ban": 50,
                    "events": {
                        "m.room.name": 100,
                        "m.room.power_levels": 100
                    },
                    "events_default": 0,
                    "kick": 50,
                    "redact": 50,
                    "state_default": 50,
                    "users": {
                        "@example:localhost": 100
                    },
                    "users_default": 0
                },
                state_key: "",
                room_id: roomId,
                sender: "@example:localhost",
                type: "m.room.power_levels",
                event_id: "test2"
            };
            validPowerLevels = JSON.parse(JSON.stringify(basePowerLevelEvent));
            validPowerLevels.content.users[userId] = 100;
            invalidPowerLevels = basePowerLevelEvent;
        });

        it("should directly send the event if it thinks power levels are ok",
        function() {
            client.sendStateEvent.and.returnValue(Promise.resolve({}));

            intent.onEvent(validPowerLevels);
            return intent.setRoomTopic(roomId, "Hello world").then(function() {
                expect(client.sendStateEvent).toHaveBeenCalledWith(
                    roomId, "m.room.topic", {topic: "Hello world"}, ""
                );
            })
        });

        it("should get the power levels before sending if it doesn't know them",
        function() {
            client.sendStateEvent.and.returnValue(Promise.resolve({}));
            client.getStateEvent.and.returnValue(
                Promise.resolve(validPowerLevels.content)
            );

            return intent.setRoomTopic(roomId, "Hello world").then(function() {
                expect(client.getStateEvent).toHaveBeenCalledWith(
                    roomId, "m.room.power_levels", ""
                );
                expect(client.sendStateEvent).toHaveBeenCalledWith(
                    roomId, "m.room.topic", {topic: "Hello world"}, ""
                );
            })
        });

        it("should modify power levels before sending if client is too low",
        function() {
            client.sendStateEvent.and.callFake(function() {
                if (botClient.setPowerLevel.calls.count() > 0) {
                    return Promise.resolve({});
                }
                return Promise.reject({
                    errcode: "M_FORBIDDEN",
                    error: "Not enough powaaaaaa"
                });
            });
            botClient.setPowerLevel.and.returnValue(Promise.resolve({}));
            // give the power to the bot
            invalidPowerLevels.content.users[botUserId] = 100;
            intent.onEvent(invalidPowerLevels);

            return intent.setRoomTopic(roomId, "Hello world").then(function() {
                expect(client.sendStateEvent).toHaveBeenCalledWith(
                    roomId, "m.room.topic", {topic: "Hello world"}, ""
                );
                expect(botClient.setPowerLevel).toHaveBeenCalledWith(
                    roomId, userId, 50, jasmine.any(Object)
                );
            })
        });

        it("should fail if the bot cannot modify power levels and the client is too low",
        function() {
            // bot has NO power
            intent.onEvent(invalidPowerLevels);

            return intent.setRoomTopic(roomId, "Hello world").catch(function() {
                expect(client.sendStateEvent).not.toHaveBeenCalled();
                expect(botClient.setPowerLevel).not.toHaveBeenCalled();
            })
        });
    });

    describe("sending message events", function() {
        const content = {
            body: "hello world",
            msgtype: "m.text",
        };

        beforeEach(function() {
            intent = new Intent(client, botClient, {
                ...alreadyRegistered,
                dontCheckPowerLevel: true,
            });
            // not interested in joins, so no-op them.
            intent.onEvent({
                event_id: "test",
                type: "m.room.member",
                state_key: userId,
                room_id: roomId,
                content: {
                    membership: "join"
                }
            });
        });

        it("should immediately try to send the event if joined/have pl", function() {
            client.sendEvent.and.returnValue(Promise.resolve({
                event_id: "$abra:kadabra"
            }));
            return intent.sendMessage(roomId, content).then(function() {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).not.toHaveBeenCalled();
            });
        });

        it("should fail if get an error that isn't M_FORBIDDEN", function() {
            client.sendEvent.and.callFake(() => Promise.reject({
                error: "Oh no",
                errcode: "M_UNKNOWN"
            }));
            return intent.sendMessage(roomId, content).catch(function() {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).not.toHaveBeenCalled();
            });
        });

        it("should try to join the room on M_FORBIDDEN then resend", function() {
            let isJoined = false;
            client.sendEvent.and.callFake(function() {
                if (isJoined) {
                    return Promise.resolve({
                        event_id: "$12345:6789"
                    });
                }
                return Promise.reject({
                    error: "You are not joined",
                    errcode: "M_FORBIDDEN"
                });
            });
            client.joinRoom.and.callFake(function(joinRoomId) {
                isJoined = true;
                return Promise.resolve({
                    room_id: joinRoomId,
                });
            });
            return intent.sendMessage(roomId, content).then(function(eventId) {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false });
                expect(eventId).toEqual({event_id: "$12345:6789"});
            });
        });

        it("should fail if the join on M_FORBIDDEN fails", function() {
            client.sendEvent.and.callFake(function() {
                return Promise.reject({
                    error: "You are not joined",
                    errcode: "M_FORBIDDEN"
                });
            });
            client.joinRoom.and.callFake(() => Promise.reject({
                error: "Never!",
                errcode: "M_YOU_ARE_A_FISH"
            }));
            return intent.sendMessage(roomId, content).catch(function() {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false });
            });
        });

        it("should fail if the resend after M_FORBIDDEN fails", function() {
            let isJoined = false;
            client.sendEvent.and.callFake(function() {
                if (isJoined) {
                    return Promise.reject({
                        error: "Internal Server Error",
                        errcode: "M_WHOOPSIE",
                    });
                }
                return Promise.reject({
                    error: "You are not joined",
                    errcode: "M_FORBIDDEN",
                });
            });
            client.joinRoom.and.callFake(function(joinRoomId) {
                isJoined = true;
                return Promise.resolve({
                    room_id: joinRoomId,
                });
            });
            return intent.sendMessage(roomId, content).catch(function() {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false });
            });
        });
    });

    describe("signaling bridge error", function() {
        const reason = "m.event_not_handled"
        let affectedUsers, eventId, bridge;

        beforeEach(function() {
            intent = new Intent(client, botClient, {
                ...alreadyRegistered,
                dontCheckPowerLevel: true,
            });
            // not interested in joins, so no-op them.
            intent.onEvent({
                event_id: "test",
                type: "m.room.member",
                state_key: userId,
                room_id: roomId,
                content: {
                    membership: "join"
                }
            });
            eventId = "$random:event.id";
            bridge = "International Pidgeon Post";
            affectedUsers = ["@_pidgeonpost_.*:home.server"];
        });

        it("should send an event", function() {
            client.sendEvent.and.returnValue(Promise.resolve({
                event_id: "$abra:kadabra"
            }));
            return intent
            .unstableSignalBridgeError(roomId, eventId, bridge, reason, affectedUsers)
            .then(() => {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId,
                    "de.nasnotfound.bridge_error",
                    {
                        "network_name": bridge,
                        "reason": reason,
                        "affected_users": affectedUsers,
                        "m.relates_to": {
                            "rel_type": "m.reference",
                            "event_id": eventId
                        }
                    }
                );
                expect(client.joinRoom).not.toHaveBeenCalled();
            });
        });
    });
});
<commit_msg>Fix test<commit_after>const { Intent } = require("../..");
const log = require("../log");

describe("Intent", function() {
    let intent, client, botClient;
    const userId = "@alice:bar";
    const botUserId = "@bot:user";
    const roomId = "!foo:bar";
    const alreadyRegistered = {
        registered: true
    };

    beforeEach(
    /** @this */
    function() {
        log.beforeEach(this);
        const clientFields = [
            "credentials", "joinRoom", "invite", "leave", "ban", "unban",
            "kick", "getStateEvent", "setPowerLevel", "sendTyping", "sendEvent",
            "sendStateEvent", "setDisplayName", "setAvatarUrl",
        ];
        client = jasmine.createSpyObj("client", clientFields);
        client.credentials.userId = userId;
        botClient = jasmine.createSpyObj("botClient", clientFields);
        botClient.credentials.userId = botUserId;
        intent = new Intent(client, botClient, alreadyRegistered);
    });

    describe("join", function() {

        it("should /join/$ROOMID if it doesn't know it is already joined",
        function() {
            client.joinRoom.and.callFake(() => Promise.resolve({roomId: roomId}));
            return intent.join(roomId).then(function(resultRoomId) {
                expect(client.joinRoom).toHaveBeenCalledWith(
                    roomId, { syncRoom: false }
                );
                expect(resultRoomId).toBe(roomId);
            });
        });

        it("should no-op if it knows it is already joined", function() {
            intent.onEvent({
                event_id: "test",
                type: "m.room.member",
                state_key: userId,
                room_id: roomId,
                content: {
                    membership: "join"
                }
            });
            return intent.join(roomId).then(function(resultRoomId) {
                expect(resultRoomId).toBe(roomId);
                expect(client.joinRoom).not.toHaveBeenCalled();
            });
        });

        it("should fail if the join returned an error other than forbidden",
        function() {
            client.joinRoom.and.callFake(() => Promise.reject({
                errcode: "M_YOU_ARE_A_FISH",
                error: "you're a fish"
            }));
            return intent.join(roomId).catch(function() {
                expect(client.joinRoom).toHaveBeenCalled();
            });
        });

        describe("client join failed", function() {

            it("should make the bot invite then the client join", function() {
                client.joinRoom.and.callFake(function() {
                    if (botClient.invite.calls.count() === 0) {
                        return Promise.reject({
                            errcode: "M_FORBIDDEN",
                            error: "Join first"
                        });
                    }
                    return Promise.resolve({roomId: roomId});
                });
                botClient.invite.and.callFake(() => Promise.resolve({}));

                return intent.join(roomId).then(function(resultRoomId) {
                    expect(client.joinRoom).toHaveBeenCalledWith(
                        roomId, { syncRoom: false }
                    );
                    expect(botClient.invite).toHaveBeenCalledWith(roomId, userId);
                    expect(resultRoomId).toBe(roomId);
                });
            });

            describe("bot invite failed", function() {
                it("should make the bot join then invite then the client join",
                function() {
                    client.joinRoom.and.callFake(function() {
                        if (botClient.invite.calls.count() === 0) {
                            return Promise.reject({
                                errcode: "M_FORBIDDEN",
                                error: "Join first"
                            });
                        }
                        return Promise.resolve({});
                    });
                    botClient.invite.and.callFake(function() {
                        if (botClient.joinRoom.calls.count() === 0) {
                            return Promise.reject({
                                errcode: "M_FORBIDDEN",
                                error: "Join first"
                            });
                        }
                        return Promise.resolve({});
                    });
                    botClient.joinRoom.and.callFake(() => Promise.resolve({roomId: roomId}));

                    return intent.join(roomId).then(function(resultRoomId) {
                        expect(client.joinRoom).toHaveBeenCalledWith(
                            roomId, { syncRoom: false }
                        );
                        expect(botClient.invite).toHaveBeenCalledWith(roomId, userId);
                        expect(botClient.joinRoom).toHaveBeenCalledWith(
                            roomId, { syncRoom: false }
                        );
                        expect(resultRoomId).toBe(roomId);

                    });
                });

                it("should give up if the bot cannot join the room", function() {
                    client.joinRoom.and.callFake(() => Promise.reject({
                        errcode: "M_FORBIDDEN",
                        error: "Join first"
                    }));
                    botClient.invite.and.callFake(() => Promise.reject({
                        errcode: "M_FORBIDDEN",
                        error: "No invites kthx"
                    }));
                    botClient.joinRoom.and.callFake(() => Promise.reject({
                        errcode: "M_FORBIDDEN",
                        error: "No bots allowed!"
                    }));

                    return intent.join(roomId).catch(function() {
                        expect(client.joinRoom).toHaveBeenCalledWith(
                            roomId, { syncRoom: false }
                        );
                        expect(botClient.invite).toHaveBeenCalledWith(roomId, userId);
                        expect(botClient.joinRoom).toHaveBeenCalledWith(
                            roomId, { syncRoom: false }
                        );
                    });
                });
            });
        });
    });

    describe("sending state events", function() {
        let validPowerLevels, invalidPowerLevels;

        beforeEach(() => {
            // not interested in joins, so no-op them.
            intent.onEvent({
                event_id: "test",
                type: "m.room.member",
                state_key: userId,
                room_id: roomId,
                content: {
                    membership: "join"
                }
            });

            const basePowerLevelEvent = {
                content: {
                    "ban": 50,
                    "events": {
                        "m.room.name": 100,
                        "m.room.power_levels": 100
                    },
                    "events_default": 0,
                    "kick": 50,
                    "redact": 50,
                    "state_default": 50,
                    "users": {
                        "@example:localhost": 100
                    },
                    "users_default": 0
                },
                state_key: "",
                room_id: roomId,
                sender: "@example:localhost",
                type: "m.room.power_levels",
                event_id: "test2"
            };
            validPowerLevels = JSON.parse(JSON.stringify(basePowerLevelEvent));
            validPowerLevels.content.users[userId] = 100;
            invalidPowerLevels = basePowerLevelEvent;
        });

        it("should directly send the event if it thinks power levels are ok",
        function() {
            client.sendStateEvent.and.returnValue(Promise.resolve({}));

            intent.onEvent(validPowerLevels);
            return intent.setRoomTopic(roomId, "Hello world").then(function() {
                expect(client.sendStateEvent).toHaveBeenCalledWith(
                    roomId, "m.room.topic", {topic: "Hello world"}, ""
                );
            })
        });

        it("should get the power levels before sending if it doesn't know them",
        function() {
            client.sendStateEvent.and.returnValue(Promise.resolve({}));
            client.getStateEvent.and.returnValue(
                Promise.resolve(validPowerLevels.content)
            );

            return intent.setRoomTopic(roomId, "Hello world").then(function() {
                expect(client.getStateEvent).toHaveBeenCalledWith(
                    roomId, "m.room.power_levels", ""
                );
                expect(client.sendStateEvent).toHaveBeenCalledWith(
                    roomId, "m.room.topic", {topic: "Hello world"}, ""
                );
            })
        });

        it("should modify power levels before sending if client is too low",
        function() {
            client.sendStateEvent.and.callFake(function() {
                if (botClient.setPowerLevel.calls.count() > 0) {
                    return Promise.resolve({});
                }
                return Promise.reject({
                    errcode: "M_FORBIDDEN",
                    error: "Not enough powaaaaaa"
                });
            });
            botClient.setPowerLevel.and.returnValue(Promise.resolve({}));
            // give the power to the bot
            invalidPowerLevels.content.users[botUserId] = 100;
            intent.onEvent(invalidPowerLevels);

            return intent.setRoomTopic(roomId, "Hello world").then(function() {
                expect(client.sendStateEvent).toHaveBeenCalledWith(
                    roomId, "m.room.topic", {topic: "Hello world"}, ""
                );
                expect(botClient.setPowerLevel).toHaveBeenCalledWith(
                    roomId, userId, 50, jasmine.any(Object)
                );
            })
        });

        it("should fail if the bot cannot modify power levels and the client is too low",
        function() {
            // bot has NO power
            intent.onEvent(invalidPowerLevels);

            return intent.setRoomTopic(roomId, "Hello world").catch(function() {
                expect(client.sendStateEvent).not.toHaveBeenCalled();
                expect(botClient.setPowerLevel).not.toHaveBeenCalled();
            })
        });
    });

    describe("sending message events", function() {
        const content = {
            body: "hello world",
            msgtype: "m.text",
        };

        beforeEach(function() {
            intent = new Intent(client, botClient, {
                ...alreadyRegistered,
                dontCheckPowerLevel: true,
            });
            // not interested in joins, so no-op them.
            intent.onEvent({
                event_id: "test",
                type: "m.room.member",
                state_key: userId,
                room_id: roomId,
                content: {
                    membership: "join"
                }
            });
        });

        it("should immediately try to send the event if joined/have pl", function() {
            client.sendEvent.and.returnValue(Promise.resolve({
                event_id: "$abra:kadabra"
            }));
            return intent.sendMessage(roomId, content).then(function() {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).not.toHaveBeenCalled();
            });
        });

        it("should fail if get an error that isn't M_FORBIDDEN", function() {
            client.sendEvent.and.callFake(() => Promise.reject({
                error: "Oh no",
                errcode: "M_UNKNOWN"
            }));
            return intent.sendMessage(roomId, content).catch(function() {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).not.toHaveBeenCalled();
            });
        });

        it("should try to join the room on M_FORBIDDEN then resend", function() {
            let isJoined = false;
            client.sendEvent.and.callFake(function() {
                if (isJoined) {
                    return Promise.resolve({
                        event_id: "$12345:6789"
                    });
                }
                return Promise.reject({
                    error: "You are not joined",
                    errcode: "M_FORBIDDEN"
                });
            });
            client.joinRoom.and.callFake(function(joinRoomId) {
                isJoined = true;
                return Promise.resolve({
                    roomId: joinRoomId,
                });
            });
            return intent.sendMessage(roomId, content).then(function(eventId) {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false });
                expect(eventId).toEqual({event_id: "$12345:6789"});
            });
        });

        it("should fail if the join on M_FORBIDDEN fails", function() {
            client.sendEvent.and.callFake(function() {
                return Promise.reject({
                    error: "You are not joined",
                    errcode: "M_FORBIDDEN"
                });
            });
            client.joinRoom.and.callFake(() => Promise.reject({
                error: "Never!",
                errcode: "M_YOU_ARE_A_FISH"
            }));
            return intent.sendMessage(roomId, content).catch(function() {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false });
            });
        });

        it("should fail if the resend after M_FORBIDDEN fails", function() {
            let isJoined = false;
            client.sendEvent.and.callFake(function() {
                if (isJoined) {
                    return Promise.reject({
                        error: "Internal Server Error",
                        errcode: "M_WHOOPSIE",
                    });
                }
                return Promise.reject({
                    error: "You are not joined",
                    errcode: "M_FORBIDDEN",
                });
            });
            client.joinRoom.and.callFake(function(joinRoomId) {
                isJoined = true;
                return Promise.resolve({
                    roomId: joinRoomId,
                });
            });
            return intent.sendMessage(roomId, content).catch(function() {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId, "m.room.message", content
                );
                expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false });
            });
        });
    });

    describe("signaling bridge error", function() {
        const reason = "m.event_not_handled"
        let affectedUsers, eventId, bridge;

        beforeEach(function() {
            intent = new Intent(client, botClient, {
                ...alreadyRegistered,
                dontCheckPowerLevel: true,
            });
            // not interested in joins, so no-op them.
            intent.onEvent({
                event_id: "test",
                type: "m.room.member",
                state_key: userId,
                room_id: roomId,
                content: {
                    membership: "join"
                }
            });
            eventId = "$random:event.id";
            bridge = "International Pidgeon Post";
            affectedUsers = ["@_pidgeonpost_.*:home.server"];
        });

        it("should send an event", function() {
            client.sendEvent.and.returnValue(Promise.resolve({
                event_id: "$abra:kadabra"
            }));
            return intent
            .unstableSignalBridgeError(roomId, eventId, bridge, reason, affectedUsers)
            .then(() => {
                expect(client.sendEvent).toHaveBeenCalledWith(
                    roomId,
                    "de.nasnotfound.bridge_error",
                    {
                        "network_name": bridge,
                        "reason": reason,
                        "affected_users": affectedUsers,
                        "m.relates_to": {
                            "rel_type": "m.reference",
                            "event_id": eventId
                        }
                    }
                );
                expect(client.joinRoom).not.toHaveBeenCalled();
            });
        });
    });
});
<|endoftext|>


<commit_before>/*
 * Vector Tests
 */
require([ "vector" ], function (Vector) {
  QUnit.test("Vector", function () {
    var vec, ret, i;

    QUnit.deepEqual(Vector.Copy([]), [], "copy empty vector");

    vec = [ 1, 2, 3, 4, 5 ];
    QUnit.deepEqual(Vector.Copy(vec), vec, "copy populated vector");

    vec = [ 1 ];
    vec[4] = 5;
    QUnit.deepEqual(Vector.Copy(vec), vec, "copy sparse vector");

    QUnit.equal(Vector.Dot([], []), 0, "dow with both operands empty");
    QUnit.equal(Vector.Dot([ 1, 2, 3, 4, 5 ], []), 0, "dot operand a empty");
    QUnit.equal(Vector.Dot([], [ 1, 2, 3, 4, 5 ]), 0, "dow operand b empty");
    QUnit.equal(Vector.Dot([ 1, 2, 3 ], [ 3, 2, 1 ]), 10,
        "dot return value check");

    vec = [];
    vec[5] = 0;
    ret = Vector.Fill(vec);

    QUnit.equal(ret, vec, "Fill return value provided");
    QUnit.equal(vec.length, 6, "Fill vector size retained");
    for (i = 0; i < 5; i += 1) {
      QUnit.equal(vec[i], 0, "Fill element " + i + " successfully filled");
    }

    QUnit.equal(vec[5], 0, "Fill preset element retained");

    vec = [ 1, 2, 3 ];
    ret = Vector.Scale(vec, 1);
    QUnit.deepEqual(ret, vec, "Scale identity");
    QUnit.deepEqual(Vector.Scale([ 1, 2, 3 ], 0), [ 0, 0, 0 ],
        "Scale by 0 retains size");
    QUnit.deepEqual(Vector.Scale([ 1, 2, 0 ], -0.3), [ -0.3, 2 * -0.3, 0 ],
        "Scale negative float factor");

    QUnit.equal(Vector.Sum([]), 0, "Sum empty vector");
    QUnit.equal(Vector.Sum([ 1, 2, 3, 4 ]), 10, "Sum populated vector");

    vec = [];
    vec[3] = 5;
    vec[6] = 3;
    QUnit.equal(Vector.Sum(vec), 8, "Sum sparse vector");
  });
});

/*
 * FullMatrix Tests
 */
require([ "fullmatrix" ], function (FullMatrix) {
  QUnit.test("FullMatrix", function () {
    // constructor validation
    var a, b;

    a = new FullMatrix();
    QUnit.equal(a.size, 0, "empty size initialization");
    QUnit.deepEqual(a.array, [], "empty array initialization");

    a = new FullMatrix(5);
    QUnit.equal(a.size, 5, "prefixed size initialization");
    QUnit.deepEqual(a.array, [], "prefixed array initialization");

    // clear
    b = new FullMatrix(8);
    b.clear(5);
    QUnit.deepEqual(b, a, "clear validation");

    // extend
    b = (new FullMatrix(4)).extend();
    QUnit.deepEqual(b, a, "extend() validation");

    b = (new FullMatrix(4)).extend(1);
    QUnit.deepEqual(b, a, "extend(1) validation");

    b = (new FullMatrix(5)).extend(0);
    QUnit.deepEqual(b, a, "extend(0) validation");

    b = (new FullMatrix(0)).extend(5);
    QUnit.deepEqual(b, a, "extend(0) validation");

    // clone, get and set
    a.clear(3);
    a.array[0] = [ 1, 2, 3 ];
    a.array[1] = [];
    a.array[1][2] = 4;

    QUnit.deepEqual(a.clone(), a, "clone");

    b.clear(3);
    b.set(0, 0, 1).set(0, 1, 2).set(0, 2, 3).set(1, 2, 4);
    QUnit.deepEqual(b, a, "chained set() commands");

    delete a.array[1][2];
    b.set(1, 2, 0);
    QUnit.deepEqual(b, a, "set(0) undefines the element");

    QUnit.equal(b.get(0, 0) === 1 && b.get(0, 1) === 2 && b.get(0, 2) === 3
        && b.get(0, 4) === 0 && b.get(1, 0) === 0 && b.get(1, 2) === 0
        && b.get(2, 3) === 0 && b.get(4, 4) === 0, true, "get QUnit.test");

    // erase
    a = new FullMatrix(3);
    a.array = [ [ 1, 2, 3 ], [ 4, 5 ], [ undefined, undefined, 7 ] ];
    b = new FullMatrix(2);
    b.array = [ [ 1, 3 ], [ undefined, 7 ] ];
    QUnit.deepEqual((new FullMatrix()).erase(0), new FullMatrix(),
        "erase row from matrix of size 0");
    QUnit.deepEqual((new FullMatrix(3)).erase(0), new FullMatrix(2),
        "erase row from empty matrix");
    QUnit.deepEqual(a.erase(1), b, "erase row from sparse matrix");
  });
});

/*
 * HalfMatrix Tests
 */
require([ "halfmatrix" ], function (HalfMatrix) {
  QUnit.test("HalfMatrix", function () {
    var a, b;

    // constructor validation
    a = new HalfMatrix();
    QUnit.equal(a.size, 0, "empty size initialization");
    QUnit.deepEqual(a.array, [], "empty array initialization");

    a = new HalfMatrix(0, 5);
    QUnit.equal(a.size, 5, "prefixed size initialization");
    QUnit.deepEqual(a.array, [], "prefixed array initialization");

    // clear
    b = new HalfMatrix(0, 8);
    b.clear(5);
    QUnit.deepEqual(b, a, "clear validation");

    // extend
    b = (new HalfMatrix(0, 4)).extend();
    QUnit.deepEqual(b, a, "extend() validation");

    b = (new HalfMatrix(0, 4)).extend(1);
    QUnit.deepEqual(b, a, "extend(1) validation");

    b = (new HalfMatrix(0, 5)).extend(0);
    QUnit.deepEqual(b, a, "extend(0) validation");

    b = (new HalfMatrix(0, 0)).extend(5);
    QUnit.deepEqual(b, a, "extend(0) validation");

    // clone, get and set
    a.clear(3);
    a.array = [ [ 1 ], [ undefined, 2 ], [ 3, 4 ] ];

    QUnit.deepEqual(a.clone(), a, "clone");

    b.clear(3);
    b.set(0, 0, 1).set(0, 2, 5).set(1, 1, 2).set(2, 0, 3).set(2, 1, 4);
    QUnit.deepEqual(b, a, "chained set() commands (top half ignored)");

    delete a.array[1][2];
    b.set(1, 2, 0);
    QUnit.deepEqual(b, a, "set(0) undefines the element");

    QUnit.ok(b.get(0, 0) === 1 && b.get(1, 0) === 0 && b.get(1, 1) === 2
        && b.get(2, 0) === 3 && b.get(2, 1) === 4 && b.get(2, 2) === 0,
        "get QUnit.test");

    // erase
    a = new HalfMatrix(0, 3);
    a.array = [ [ 1, 2, 3 ], [ 4, 5 ], [ undefined, undefined, 7 ] ];
    b = new HalfMatrix(0, 2);
    b.array = [ [ 1, 3 ], [ undefined, 7 ] ];
    QUnit.deepEqual((new HalfMatrix()).erase(0), new HalfMatrix(),
        "erase row from matrix of size 0");
    QUnit.deepEqual((new HalfMatrix(3)).erase(0), new HalfMatrix(2),
        "erase row from empty matrix");
    QUnit.deepEqual(a.erase(1), b, "erase row from sparse matrix");

    // HalfMatrix.type QUnit.tests
    // empty
    a = new HalfMatrix(HalfMatrix.empty);
    b = new HalfMatrix(0);
    QUnit.deepEqual(b, a, "HalfMatrix.empty constant");

    a.extend(5).set(2, 2, 1).set(0, 2, 2).set(2, 0, 3);
    QUnit.ok(a.get(2, 2) === 1 && a.get(0, 2) === 0 && a.get(2, 0) === 3,
        "empty get()");

    // mirrored
    a = new HalfMatrix(HalfMatrix.mirrored);
    b = new HalfMatrix(1);
    QUnit.deepEqual(b, a, "HalfMatrix.mirrored constant");

    a.extend(5).set(2, 2, 1).set(0, 2, 2).set(2, 0, 3);
    QUnit.ok(a.get(2, 2) === 1 && a.get(0, 2) === 3 && a.get(2, 0) === 3,
        "mirrored get()");

    // negated
    a = new HalfMatrix(HalfMatrix.negated);
    b = new HalfMatrix(-1);
    QUnit.deepEqual(b, a, "HalfMatrix.negated constant");

    a.extend(5).set(2, 2, 1).set(0, 2, 2).set(2, 0, 3);
    QUnit.ok(a.get(2, 2) === 1 && a.get(0, 2) === -3 && a.get(2, 0) === 3,
        "negated get()");
  });
});

/*
 * Matrix Tests
 */
require([ "matrix", "fullmatrix", "vector" ], function (Matrix, FullMatrix,
    Vector) {
  QUnit.test("Matrix", function () {
    var a, b, vec, out, transpose, expected;

    // using FullMatrix implementation due to
    // generality
    a = new FullMatrix(3);
    a.array = [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ];
    out = new FullMatrix(5);

    transpose = new FullMatrix(3);
    transpose.array = [ [ 1, 4, 7 ], [ 2, 5, 8 ], [ 3, 6, 9 ] ];
    QUnit.deepEqual(Matrix.Transpose(a.clone()), transpose,
        "Transpose: return value validation");

    out = new FullMatrix(5);

    b = new FullMatrix(2);
    QUnit.deepEqual(Matrix.Mult(a, b, out), undefined,
        "Matrix.Mult: abort on different sizes Matrix");
    QUnit.deepEqual(out, new FullMatrix(),
        "Matrix.Mult: output matrix cleared on abort");

    b.extend();
    b.array = [ undefined, [ 7, 9, 5 ], [ undefined, 3, undefined ] ];
    expected = new FullMatrix(3);
    expected.array = [ [ 14, 27, 10 ], [ 35, 63, 25 ], [ 56, 99, 40 ] ];
    QUnit.deepEqual(Matrix.Mult(a, b, out), expected, "Matrix Multiplication");

    vec = [ 1, 2, 3 ];
    a = new FullMatrix();
    QUnit.deepEqual(Matrix.MultVec(b, []), [ 0, 0, 0 ],
        "MultVec with empty vector");
    QUnit.deepEqual(Matrix.VecMult([], b), [ 0, 0, 0 ],
        "VecMult with empty vector");
    QUnit.deepEqual(Matrix.MultVec(a, vec), [], "MultVec with empty matrix");
    QUnit.deepEqual(Matrix.VecMult(vec, a), [], "VecMult with empty matrix");
    QUnit.deepEqual(Matrix.MultVec(b, vec), [ 0, 40, 6 ], "MultVec check");
    QUnit.deepEqual(Matrix.VecMult(vec, b), [ 14, 27, 10 ], "VecMult check");

    // GetRow und GetCol
    QUnit.deepEqual(Matrix.GetRow(b, 1), [ 7, 9, 5 ],
        "GetRow with populated row");

    vec = Matrix.GetRow(b, 2);
    QUnit.deepEqual(vec, [ 0, 3, 0 ], "GetRow with sparse row");

    QUnit.deepEqual(Matrix.GetRow(b, 0), [ 0, 0, 0 ], "GetRow with empty row");

    out = Matrix.Transpose(b.clone());
    QUnit.deepEqual(Matrix.GetCol(out, 1), [ 7, 9, 5 ],
        "GetCol with populated col");
    QUnit.deepEqual(Matrix.GetCol(out, 2), [ 0, 3, 0 ],
        "GetCol with sparse col");
    vec = Matrix.GetCol(out, 0);
    QUnit.deepEqual(vec, [ 0, 0, 0 ], "GetCol with empty col");

    // Sums
    QUnit.deepEqual(Matrix.RowSum(b, 1), 21, "RowSum populated row");
    QUnit.deepEqual(Matrix.RowSum(b, 2), 3, "RowSum sparse row");
    QUnit.deepEqual(Matrix.RowSum(b, 0), 0, "RowSum empty row");

    QUnit.deepEqual(Matrix.ColSum(out, 1), 21, "ColSum populated row");
    QUnit.deepEqual(Matrix.ColSum(out, 2), 3, "ColSum sparse row");
    QUnit.deepEqual(Matrix.ColSum(out, 0), 0, "ColSum empty row");

    QUnit.deepEqual(Matrix.RowSums(a), [], "RowSums with matrix of size 0");
    QUnit.deepEqual(Matrix.RowSums(b), [ 0, 21, 3 ],
        "RowSums with sparse matrix");
    QUnit.deepEqual(Matrix.ColSums(a), [], "ColSums with matrix of size 0");
    QUnit.deepEqual(Matrix.ColSums(out), [ 0, 21, 3 ],
        "ColSums with sparse matrix");
  });
});

/*
 * Map Tests
 */
require([ "map" ], function (Map) {
  QUnit.test("Map", function () {
    var map, a, b, c;

    map = new Map();
    QUnit.equal(map.size(), 0, "empty map size");

    a = map.insert(5);
    b = map.insert(4);
    c = map.insert(3);

    QUnit.equal(map.size(), 3, "full map size (absolute)");
    QUnit.equal(map.size(), c + 1, "full map size (relative)");

    QUnit.equal(map.find(1), -1, "correct failure on missing element");
    QUnit.equal(map.find(5), a, "first index");
    QUnit.equal(map.find(4), b, "second index");
    QUnit.equal(map.find(3), c, "third index");
    QUnit.equal(map.at(a), 5, "first value");
    QUnit.equal(map.at(b), 4, "second value");
    QUnit.equal(map.at(c), 3, "third value");

    map.erase(1);
    QUnit.equal(map.size(), 2, "reduced map size");
    QUnit.equal(map.at(a), 5, "first reduced value");
    QUnit.equal(map.at(b), 3, "third reduced value");
    QUnit.equal(map.find(3), b, "third reduced index");

    map.remove(5);
    QUnit.equal(map.size(), 1, "further reduced map size");
    QUnit.equal(map.find(3), 0, "third further reduced index");

    map.remove(3);
    QUnit.equal(map.size(), 0, "completely reduced map size");
  });
});
<commit_msg>simple result tests<commit_after>/*
 * Vector Tests
 */
require([ "vector" ], function (Vector) {
  QUnit.test("Vector", function () {
    var vec, ret, i;

    QUnit.deepEqual(Vector.Copy([]), [], "copy empty vector");

    vec = [ 1, 2, 3, 4, 5 ];
    QUnit.deepEqual(Vector.Copy(vec), vec, "copy populated vector");

    vec = [ 1 ];
    vec[4] = 5;
    QUnit.deepEqual(Vector.Copy(vec), vec, "copy sparse vector");

    QUnit.equal(Vector.Dot([], []), 0, "dow with both operands empty");
    QUnit.equal(Vector.Dot([ 1, 2, 3, 4, 5 ], []), 0, "dot operand a empty");
    QUnit.equal(Vector.Dot([], [ 1, 2, 3, 4, 5 ]), 0, "dow operand b empty");
    QUnit.equal(Vector.Dot([ 1, 2, 3 ], [ 3, 2, 1 ]), 10,
        "dot return value check");

    vec = [];
    vec[5] = 0;
    ret = Vector.Fill(vec);

    QUnit.equal(ret, vec, "Fill return value provided");
    QUnit.equal(vec.length, 6, "Fill vector size retained");
    for (i = 0; i < 5; i += 1) {
      QUnit.equal(vec[i], 0, "Fill element " + i + " successfully filled");
    }

    QUnit.equal(vec[5], 0, "Fill preset element retained");

    vec = [ 1, 2, 3 ];
    ret = Vector.Scale(vec, 1);
    QUnit.deepEqual(ret, vec, "Scale identity");
    QUnit.deepEqual(Vector.Scale([ 1, 2, 3 ], 0), [ 0, 0, 0 ],
        "Scale by 0 retains size");
    QUnit.deepEqual(Vector.Scale([ 1, 2, 0 ], -0.3), [ -0.3, 2 * -0.3, 0 ],
        "Scale negative float factor");

    QUnit.equal(Vector.Sum([]), 0, "Sum empty vector");
    QUnit.equal(Vector.Sum([ 1, 2, 3, 4 ]), 10, "Sum populated vector");

    vec = [];
    vec[3] = 5;
    vec[6] = 3;
    QUnit.equal(Vector.Sum(vec), 8, "Sum sparse vector");
  });
});

/*
 * FullMatrix Tests
 */
require([ "fullmatrix" ], function (FullMatrix) {
  QUnit.test("FullMatrix", function () {
    // constructor validation
    var a, b;

    a = new FullMatrix();
    QUnit.equal(a.size, 0, "empty size initialization");
    QUnit.deepEqual(a.array, [], "empty array initialization");

    a = new FullMatrix(5);
    QUnit.equal(a.size, 5, "prefixed size initialization");
    QUnit.deepEqual(a.array, [], "prefixed array initialization");

    // clear
    b = new FullMatrix(8);
    b.clear(5);
    QUnit.deepEqual(b, a, "clear validation");

    // extend
    b = (new FullMatrix(4)).extend();
    QUnit.deepEqual(b, a, "extend() validation");

    b = (new FullMatrix(4)).extend(1);
    QUnit.deepEqual(b, a, "extend(1) validation");

    b = (new FullMatrix(5)).extend(0);
    QUnit.deepEqual(b, a, "extend(0) validation");

    b = (new FullMatrix(0)).extend(5);
    QUnit.deepEqual(b, a, "extend(0) validation");

    // clone, get and set
    a.clear(3);
    a.array[0] = [ 1, 2, 3 ];
    a.array[1] = [];
    a.array[1][2] = 4;

    QUnit.deepEqual(a.clone(), a, "clone");

    b.clear(3);
    b.set(0, 0, 1).set(0, 1, 2).set(0, 2, 3).set(1, 2, 4);
    QUnit.deepEqual(b, a, "chained set() commands");

    delete a.array[1][2];
    b.set(1, 2, 0);
    QUnit.deepEqual(b, a, "set(0) undefines the element");

    QUnit.equal(b.get(0, 0) === 1 && b.get(0, 1) === 2 && b.get(0, 2) === 3
        && b.get(0, 4) === 0 && b.get(1, 0) === 0 && b.get(1, 2) === 0
        && b.get(2, 3) === 0 && b.get(4, 4) === 0, true, "get QUnit.test");

    // erase
    a = new FullMatrix(3);
    a.array = [ [ 1, 2, 3 ], [ 4, 5 ], [ undefined, undefined, 7 ] ];
    b = new FullMatrix(2);
    b.array = [ [ 1, 3 ], [ undefined, 7 ] ];
    QUnit.deepEqual((new FullMatrix()).erase(0), new FullMatrix(),
        "erase row from matrix of size 0");
    QUnit.deepEqual((new FullMatrix(3)).erase(0), new FullMatrix(2),
        "erase row from empty matrix");
    QUnit.deepEqual(a.erase(1), b, "erase row from sparse matrix");
  });
});

/*
 * HalfMatrix Tests
 */
require([ "halfmatrix" ], function (HalfMatrix) {
  QUnit.test("HalfMatrix", function () {
    var a, b;

    // constructor validation
    a = new HalfMatrix();
    QUnit.equal(a.size, 0, "empty size initialization");
    QUnit.deepEqual(a.array, [], "empty array initialization");

    a = new HalfMatrix(0, 5);
    QUnit.equal(a.size, 5, "prefixed size initialization");
    QUnit.deepEqual(a.array, [], "prefixed array initialization");

    // clear
    b = new HalfMatrix(0, 8);
    b.clear(5);
    QUnit.deepEqual(b, a, "clear validation");

    // extend
    b = (new HalfMatrix(0, 4)).extend();
    QUnit.deepEqual(b, a, "extend() validation");

    b = (new HalfMatrix(0, 4)).extend(1);
    QUnit.deepEqual(b, a, "extend(1) validation");

    b = (new HalfMatrix(0, 5)).extend(0);
    QUnit.deepEqual(b, a, "extend(0) validation");

    b = (new HalfMatrix(0, 0)).extend(5);
    QUnit.deepEqual(b, a, "extend(0) validation");

    // clone, get and set
    a.clear(3);
    a.array = [ [ 1 ], [ undefined, 2 ], [ 3, 4 ] ];

    QUnit.deepEqual(a.clone(), a, "clone");

    b.clear(3);
    b.set(0, 0, 1).set(0, 2, 5).set(1, 1, 2).set(2, 0, 3).set(2, 1, 4);
    QUnit.deepEqual(b, a, "chained set() commands (top half ignored)");

    delete a.array[1][2];
    b.set(1, 2, 0);
    QUnit.deepEqual(b, a, "set(0) undefines the element");

    QUnit.ok(b.get(0, 0) === 1 && b.get(1, 0) === 0 && b.get(1, 1) === 2
        && b.get(2, 0) === 3 && b.get(2, 1) === 4 && b.get(2, 2) === 0,
        "get QUnit.test");

    // erase
    a = new HalfMatrix(0, 3);
    a.array = [ [ 1, 2, 3 ], [ 4, 5 ], [ undefined, undefined, 7 ] ];
    b = new HalfMatrix(0, 2);
    b.array = [ [ 1, 3 ], [ undefined, 7 ] ];
    QUnit.deepEqual((new HalfMatrix()).erase(0), new HalfMatrix(),
        "erase row from matrix of size 0");
    QUnit.deepEqual((new HalfMatrix(3)).erase(0), new HalfMatrix(2),
        "erase row from empty matrix");
    QUnit.deepEqual(a.erase(1), b, "erase row from sparse matrix");

    // HalfMatrix.type QUnit.tests
    // empty
    a = new HalfMatrix(HalfMatrix.empty);
    b = new HalfMatrix(0);
    QUnit.deepEqual(b, a, "HalfMatrix.empty constant");

    a.extend(5).set(2, 2, 1).set(0, 2, 2).set(2, 0, 3);
    QUnit.ok(a.get(2, 2) === 1 && a.get(0, 2) === 0 && a.get(2, 0) === 3,
        "empty get()");

    // mirrored
    a = new HalfMatrix(HalfMatrix.mirrored);
    b = new HalfMatrix(1);
    QUnit.deepEqual(b, a, "HalfMatrix.mirrored constant");

    a.extend(5).set(2, 2, 1).set(0, 2, 2).set(2, 0, 3);
    QUnit.ok(a.get(2, 2) === 1 && a.get(0, 2) === 3 && a.get(2, 0) === 3,
        "mirrored get()");

    // negated
    a = new HalfMatrix(HalfMatrix.negated);
    b = new HalfMatrix(-1);
    QUnit.deepEqual(b, a, "HalfMatrix.negated constant");

    a.extend(5).set(2, 2, 1).set(0, 2, 2).set(2, 0, 3);
    QUnit.ok(a.get(2, 2) === 1 && a.get(0, 2) === -3 && a.get(2, 0) === 3,
        "negated get()");
  });
});

/*
 * Matrix Tests
 */
require([ "matrix", "fullmatrix", "vector" ], function (Matrix, FullMatrix,
    Vector) {
  QUnit.test("Matrix", function () {
    var a, b, vec, out, transpose, expected;

    // using FullMatrix implementation due to
    // generality
    a = new FullMatrix(3);
    a.array = [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ];
    out = new FullMatrix(5);

    transpose = new FullMatrix(3);
    transpose.array = [ [ 1, 4, 7 ], [ 2, 5, 8 ], [ 3, 6, 9 ] ];
    QUnit.deepEqual(Matrix.Transpose(a.clone()), transpose,
        "Transpose: return value validation");

    out = new FullMatrix(5);

    b = new FullMatrix(2);
    QUnit.deepEqual(Matrix.Mult(a, b, out), undefined,
        "Matrix.Mult: abort on different sizes Matrix");
    QUnit.deepEqual(out, new FullMatrix(),
        "Matrix.Mult: output matrix cleared on abort");

    b.extend();
    b.array = [ undefined, [ 7, 9, 5 ], [ undefined, 3, undefined ] ];
    expected = new FullMatrix(3);
    expected.array = [ [ 14, 27, 10 ], [ 35, 63, 25 ], [ 56, 99, 40 ] ];
    QUnit.deepEqual(Matrix.Mult(a, b, out), expected, "Matrix Multiplication");

    vec = [ 1, 2, 3 ];
    a = new FullMatrix();
    QUnit.deepEqual(Matrix.MultVec(b, []), [ 0, 0, 0 ],
        "MultVec with empty vector");
    QUnit.deepEqual(Matrix.VecMult([], b), [ 0, 0, 0 ],
        "VecMult with empty vector");
    QUnit.deepEqual(Matrix.MultVec(a, vec), [], "MultVec with empty matrix");
    QUnit.deepEqual(Matrix.VecMult(vec, a), [], "VecMult with empty matrix");
    QUnit.deepEqual(Matrix.MultVec(b, vec), [ 0, 40, 6 ], "MultVec check");
    QUnit.deepEqual(Matrix.VecMult(vec, b), [ 14, 27, 10 ], "VecMult check");

    // GetRow und GetCol
    QUnit.deepEqual(Matrix.GetRow(b, 1), [ 7, 9, 5 ],
        "GetRow with populated row");

    vec = Matrix.GetRow(b, 2);
    QUnit.deepEqual(vec, [ 0, 3, 0 ], "GetRow with sparse row");

    QUnit.deepEqual(Matrix.GetRow(b, 0), [ 0, 0, 0 ], "GetRow with empty row");

    out = Matrix.Transpose(b.clone());
    QUnit.deepEqual(Matrix.GetCol(out, 1), [ 7, 9, 5 ],
        "GetCol with populated col");
    QUnit.deepEqual(Matrix.GetCol(out, 2), [ 0, 3, 0 ],
        "GetCol with sparse col");
    vec = Matrix.GetCol(out, 0);
    QUnit.deepEqual(vec, [ 0, 0, 0 ], "GetCol with empty col");

    // Sums
    QUnit.deepEqual(Matrix.RowSum(b, 1), 21, "RowSum populated row");
    QUnit.deepEqual(Matrix.RowSum(b, 2), 3, "RowSum sparse row");
    QUnit.deepEqual(Matrix.RowSum(b, 0), 0, "RowSum empty row");

    QUnit.deepEqual(Matrix.ColSum(out, 1), 21, "ColSum populated row");
    QUnit.deepEqual(Matrix.ColSum(out, 2), 3, "ColSum sparse row");
    QUnit.deepEqual(Matrix.ColSum(out, 0), 0, "ColSum empty row");

    QUnit.deepEqual(Matrix.RowSums(a), [], "RowSums with matrix of size 0");
    QUnit.deepEqual(Matrix.RowSums(b), [ 0, 21, 3 ],
        "RowSums with sparse matrix");
    QUnit.deepEqual(Matrix.ColSums(a), [], "ColSums with matrix of size 0");
    QUnit.deepEqual(Matrix.ColSums(out), [ 0, 21, 3 ],
        "ColSums with sparse matrix");
  });
});

/*
 * Map Tests
 */
require([ "map" ], function (Map) {
  QUnit.test("Map", function () {
    var map, a, b, c;

    map = new Map();
    QUnit.equal(map.size(), 0, "empty map size");

    a = map.insert(5);
    b = map.insert(4);
    c = map.insert(3);

    QUnit.equal(map.size(), 3, "full map size (absolute)");
    QUnit.equal(map.size(), c + 1, "full map size (relative)");

    QUnit.equal(map.find(1), -1, "correct failure on missing element");
    QUnit.equal(map.find(5), a, "first index");
    QUnit.equal(map.find(4), b, "second index");
    QUnit.equal(map.find(3), c, "third index");
    QUnit.equal(map.at(a), 5, "first value");
    QUnit.equal(map.at(b), 4, "second value");
    QUnit.equal(map.at(c), 3, "third value");

    map.erase(1);
    QUnit.equal(map.size(), 2, "reduced map size");
    QUnit.equal(map.at(a), 5, "first reduced value");
    QUnit.equal(map.at(b), 3, "third reduced value");
    QUnit.equal(map.find(3), b, "third reduced index");

    map.remove(5);
    QUnit.equal(map.size(), 1, "further reduced map size");
    QUnit.equal(map.find(3), 0, "third further reduced index");

    map.remove(3);
    QUnit.equal(map.size(), 0, "completely reduced map size");
  });
});

/*
 * Result Tests
 */
require([ "result" ], function (Result) {
  QUnit.test("Result", function () {
    var a, b, c, pa, pb, res;

    a = 1;
    b = [ 2, 3 ];
    c = [ 2, 3 ];

    pa = 5;
    pb = 13;

    res = new Result(a, b, pa, pb);

    // team tests
    QUnit.equal(res.getTeam(), undefined, "undefined team request");
    QUnit.equal(res.getTeam(0), undefined, "0 team request");
    QUnit.equal(res.getTeam(b), undefined, "array team request");
    QUnit.deepEqual(res.getTeam(2), b, "team constructed by array");
    b[1] = 5;
    QUnit.deepEqual(res.getTeam(2), c, "team copied in constructor");
    QUnit.deepEqual(res.getTeam(1), [ a ], "team constructed by integer");

    // points tests
    QUnit.equal(res.getPoints(), undefined, "undefined points request");
    QUnit.equal(res.getPoints(0), undefined, "0 points request");

    QUnit.equal(res.getPoints(1), pa, "points of first team");
    QUnit.equal(res.getPoints(1), pa, "points of second team");

    QUnit.equal(res.getNetto(), pa - pb, "netto points");
  });
});
<|endoftext|>


<commit_before>JSpec.addMatchers({

  be_tag: "jQuery(actual).is(expected) === true",

  receive_stub: {defer: true, match: function(actual, method, returnValue, times) {
    JSpec.stub(actual, method).and_return(returnValue)
    var proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate)
    JSpec.currentSpec.assertions.push(proxy)
    return proxy
  }}

})
<commit_msg>added be_in matcher to check one of multiple possible values<commit_after>JSpec.addMatchers({

  be_tag: "jQuery(actual).is(expected) === true",

  be_in: { match: function(actual) {
    var possibleExpectations = Array.prototype.slice.call(arguments, 1);
    return JSpec.any(possibleExpectations, function(each){
      return each === actual;
    });
  }},

  receive_stub: {defer: true, match: function(actual, method, returnValue, times) {
    JSpec.stub(actual, method).and_return(returnValue)
    var proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate)
    JSpec.currentSpec.assertions.push(proxy)
    return proxy
  }}

})
<|endoftext|>


<commit_before>/* jshint esversion: 6 */

var meadData = [{
        'batchID': 0,
        'type': 'Dry Ginger Ale',
        'recipeName': 'Wabozu\'',
        'bottleRangeMin': 0,
        'bottleRangeMax': 0,
        'size': '2L',
        'notes': 'Very first brew; super dry ginger ale.',
        'openingDate': '2017-09-11',
        'bottleDistribution': [
            ['1', 'JWK']
        ],
        'timeline': [
            // date, notes
            ['2012-09-09', 'Must prepared.'],
            ['2017-09-10', 'Burped.'],
            ['2017-09-11', 'Consumed.']
        ],
        'gravReadings': [
            // date, OG, notes
            ['2017-10-09', 1.1, 'Pitched'],
            ['2017-10-09', 1.09, 'Pitched']
        ],
        'images': [],
        'tastingNotes': 'Lovely -- quite spicy and dry but excellent. Towards bottom of the bottle, floculated yeast gets a little gross. Carbonation disappeared exceedingly quickly.',
    },
    {
        'batchID': 1,
        'type': 'Sweet Mead',
        'recipeName': 'Jutini\'',
        'bottleRangeMin': 1,
        'bottleRangeMax': 5,
        'size': '1 gal.',
        'notes': 'This is one of my first two meads! This mead uses a weaker yeast that should result in a sweet mead. This is a small batch and may or may not come out tasting like battery acid -- as always, longer aging makes for a better mead!

Honeys used: 3lb Crocket Raw Arizona Desert Wildflower honey, .5lb Nature Nate\'s Raw Unfiltered honey.',
        'openingDate': 'Open 2018-04-21 at the earliest; let age for a year (2018-10) or longer for optimum taste!',
        'bottleDistribution': [
            ['1', 'JKi'],
            ['2', 'JKi'],
            ['3', 'CBa'],
            ['4', 'MMa'],
            ['5', 'MBa']
        ],
        'timeline': [
            // date, notes
            ['2017-10-09 1830', 'Must prepared.'],
            ['2017-10-09 1930', 'Carboy airlocked.'],
            ['2017-10-09 2230', 'Very few bubbles; some tiny surface ones but only ~10% coverage.'],
            ['2017-10-10 0030', 'Bubble from yeast every minute or two'],
            ['2017-10-11 1415', 'Bubbles every two to three seconds; moved to warmed surroundings with towel around carboy for light and warmth']
        ],
        'gravReadings': [
            // date, OG, notes
            ['2017-10-09', 1.12, 'Pitched']
        ],
        'images': ['img/f1mustevening.jpg', 'img/mixedmusts.jpg', 'img/musts.jpg', 'img/ingredients.jpg', 'img/bundled.jpg'],
        'tastingNotes': ''
    },
    {
        'batchID': 2,
        'type': 'Semi-Dry Mead',
        'recipeName': 'Jutini-α',
        'bottleRangeMin': 6,
        'bottleRangeMax': 10,
        'size': '1 gal.',
        'notes': 'This is one of my first two meads! This mead uses a stronger yeast that should result in a semi-dry mead. This is a small batch and may or may not come out tasting like battery acid -- as always, longer aging makes for a better mead!

Honeys used: 3lb Crocket Raw Arizona Desert Wildflower honey, .5lb Nature Nate\'s Raw Unfiltered honey. Promised bottles: B6-7 JWK, B8 HLu, B9 MWe, B10 JFe.',
        'openingDate': 'Open 2018-04-21 at the earliest; let age for a year (2018-10) or longer for optimum taste!',
        'bottleDistribution': [
            ['6', 'JKi'],
            ['7', 'JKi'],
            ['8', 'HLu'],
            ['9', 'MWe'],
            ['10', 'JFe']
        ],
        'timeline': [
            // date, notes
            ['2017-10-09 1830', 'Must prepared.'],
            ['2017-10-09 1930', 'Carboy airlocked.'],
            ['2017-10-09 2230', 'Lots of tiny bubbles; 100% surface coverage and about 2mm thick.'],
            ['2017-10-10 0030', 'Creamy layer of top bubbles; bubble through airlock once every 10 seconds. These yeast are HUNGRY!'],
            ['2017-10-11 1415', 'Bubbles every second; moved to warmed surroundings with towel around carboy for light and warmth']
        ],
        'gravReadings': [
            // date, OG, notes
            ['2017-10-09', 1.11, 'Pitched']
        ],
        images: ['img/f2bubbling.gif', 'img/f2mustevening.jpg', 'img/mixedmusts.jpg', 'img/f2must.jpg', 'img/musts.jpg', 'img/ingredients.jpg', 'img/f2yeast.jpg', 'img/bundled.jpg'],
        'tastingNotes': '',
    }
];

var recipes = {
    'Wabozu\'': `Fiery ginger ale

**Volume Produced:** 2 liters

**Brew time:** 2 days

**Age time:** 1 day

### Ingredients
- 1.5 oz. finely grated fresh ginger (the younger the better; microplane suggested)
- 6 oz. table sugar
- 7.5 cups filtered water
- 1/8 teaspoon (should be around 750mg) champagne yeast (Red Star Premier Blanc formerly known as Pastuer Champagne)
- 2 tablespoons freshly squeezed lemon juice
- StarSan soln.

### Equipment/Misc
- 2L PET bottle (i.e. plastic soda bottle)

### Prep
Clean 2L PET bottle with StarSan soln.

### Recipe
Place ginger, sugar, and 1/2 cup of the water into a saucepan over medium-high heat. Stir until the sugar dissolves. Remove from heat, cover, and allow to steep for 1 hour.

Pour the syrup through a fine strainer, cheese cloth over a bowl, or t-shirt, using pressure to get all the juice out of the mixture. Rapidly chill in an ice bath and stir until the mixture reaches 70 degrees F.

Pour the syrup into a 2L PET bottle and add yeast, lemon juice, and remaining 7 cups of water. Shake vigorously for five minutes. Let sit in a dark place at room temperature.

### Rack'n'Pack
At 2 days, burp and refrigerate bottle. Allow 24 hours for yeast to fall asleep, then siphon off into another StarSan'd 2L soda bottle, leaving the flocculated yeast at the bottom of the first bottle.  Take and record gravity reading if desired. Keep chilled and drink quickly; the carbonation will not last long -- 3 days is safe.`,
    'Jutini\'': `Simple sweet mead

**Voume Produced:** 1 gallon

**Brew time:** 2 weeks primary, 2 week stabilization

**Age time:** 3 - 6 months

### Ingredients
- 3.5 lbs honey
- 1 tsp LD Carlson Nutrient (diammonium phosphate/DAP)
- 0.5 tsp LD Carlson Energizer (yeast hulls)
- 5 grams Safale US-05 yeast
- Distilled water to 1 gal.

### Equipment/Misc.
- 1 gal. carboy
- Airlock
- StarSan soln.
- 1 Campden tablet, crushed

### Prep
Clean 1 gal. carboy with StarSan soln. .1oz starsan (.6 tsp) for half gallon. Rinse until suds subside.

### Recipe
Add room temperature honey, then room temperature distilled water until volume of must is 1 gal.

Pour dry yeast into must, and add DAP with yeast hulls.

Seal carboy and shake vigorously for 4 minutes. Take and record gravity reading if desired. Airlock carboy.

Place in dark location at 60 - 75 degrees F.

After two weeks, check for fermentation completion via SG check. Once complete, rack onto crushed campden and let sit for two weeks or until clear, then bottle.`,
    'Jutini-α': `Simple semi-dry mead

**Voume Produced:** 1 gallon

**Brew time:** 2 weeks primary, 2 week stabilization

**Age time:** 3 - 6 months

### Ingredients
- 3.5 lbs honey
- 1 tsp LD Carlson Nutrient (diammonium phosphate/DAP)
- 0.5 tsp LD Carlson Energizer (yeast hulls)
- 5 grams Lavlin 71B-1122
- Distilled water to 1 gal.

### Equipment/Misc.
- 1 gal. carboy
- Airlock
- StarSan soln.
- 1 Campden tablet, crushed

### Prep
Clean 1 gal. carboy with StarSan soln. .1oz starsan (.6 tsp) for half gallon. Rinse until suds subside.

### Recipe
Add room temperature honey, then room temperature distilled water until volume of must is 1 gal.

Pour dry yeast into must, and add DAP with yeast hulls.

Seal carboy and shake vigorously for 4 minutes. Take and record gravity reading if desired. Airlock carboy.

Place in dark location at 60 - 75 degrees F.

After two weeks, check for fermentation completion via SG check. Once complete, rack onto crushed campden and let sit for two weeks or until clear, then bottle.`,
    'Soravo\'': `Reaper Capsicumel

**Voume Produced:** 1 gallon

**Brew time:** 1 month primary, 1 month oaking/secondary, 2 weeks stabilization

**Age time:** 6 months

### Ingredients
- 3 lbs honey
- 1 tsp LD Carlson Nutrient (diammonium phosphate/DAP)
- 0.5 tsp LD Carlson Energizer (yeast hulls)
- 5 grams Lavlin EC-1118
- 4 Carolina Reaper chilis
- 1 Med. toast oak spiral
- Distilled water to 1 gal.

### Equipment/Misc.
- 1 gal. carboy
- Airlock
- StarSan soln.
- 1 Campden tablet, crushed

### Prep
Clean 1 gal. carboy with StarSan soln. .1oz starsan (.6 tsp) for half gallon. Rinse until suds subside.

### Recipe
Add room temperature honey, then room temperature distilled water until volume of must is short of 1 gal. Pour dry yeast into must, and add DAP with yeast hulls.

Seal carboy and shake vigorously for 4 minutes.

Take and record gravity reading if desired.

Simmer chopped and de-seeded chilis in 1/4 cup of water for ten minutes; pour into must through muslin and tie off muslin.

Fill with water to 1 gal. Airlock carboy.

Place in dark location at 60 - 75 degrees F.

After 1 month, rack off peppers and into new vessel with oak spiral.

After another month, rack off oak and add crushed Campden tablet. Let sit for two weeks or until clear, then bottle.`,
    'Duduhu\'': `Hot ginger metheglin

**Voume Produced:** 1 gallon

**Brew time:** 2 month primary, 2 week stabilization

**Age time:** 6-12 months

### Ingredients
- 3.5 lbs honey
- 1 tsp LD Carlson Nutrient (diammonium phosphate/DAP)
- 0.5 tsp LD Carlson Energizer (yeast hulls)
- 1 lb raw ginger, peeled and cubed
- 5 grams Lavlin 71B-1122
- Distilled water to 1 gal.

### Equipment/Misc.
- 1 gal. carboy
- Airlock
- StarSan soln.
- 1 Campden tablet, crushed

### Prep
Clean 1 gal. carboy with StarSan soln. .1oz starsan (.6 tsp) for half gallon. Rinse until suds subside.

### Recipe
Add room temperature honey, then room temperature distilled water until volume of must is shy of 1 gal.

Pour dry yeast into must, and add DAP with yeast hulls.

Seal carboy and shake vigorously for 4 minutes. Take and record gravity reading if desired.

Add ginger after peeling and cubing, fill to 1 gal., put into carboy, and airlock.

Place in dark location at 60 - 75 degrees F.

After three weeks, check for fermentation completion via SG check. Once complete, rack onto crushed campden and let sit for two weeks or until clear, then bottle.`
};
<commit_msg>Change age time and put ginger in secondary<commit_after>/* jshint esversion: 6 */

var meadData = [{
        'batchID': 0,
        'type': 'Dry Ginger Ale',
        'recipeName': 'Wabozu\'',
        'bottleRangeMin': 0,
        'bottleRangeMax': 0,
        'size': '2L',
        'notes': 'Very first brew; super dry ginger ale.',
        'openingDate': '2017-09-11',
        'bottleDistribution': [
            ['1', 'JWK']
        ],
        'timeline': [
            // date, notes
            ['2012-09-09', 'Must prepared.'],
            ['2017-09-10', 'Burped.'],
            ['2017-09-11', 'Consumed.']
        ],
        'gravReadings': [
            // date, OG, notes
            ['2017-10-09', 1.1, 'Pitched'],
            ['2017-10-09', 1.09, 'Pitched']
        ],
        'images': [],
        'tastingNotes': 'Lovely -- quite spicy and dry but excellent. Towards bottom of the bottle, floculated yeast gets a little gross. Carbonation disappeared exceedingly quickly.',
    },
    {
        'batchID': 1,
        'type': 'Sweet Mead',
        'recipeName': 'Jutini\'',
        'bottleRangeMin': 1,
        'bottleRangeMax': 5,
        'size': '1 gal.',
        'notes': 'This is one of my first two meads! This mead uses a weaker yeast that should result in a sweet mead. This is a small batch and may or may not come out tasting like battery acid -- as always, longer aging makes for a better mead!

Honeys used: 3lb Crocket Raw Arizona Desert Wildflower honey, .5lb Nature Nate\'s Raw Unfiltered honey.',
        'openingDate': 'Open 2018-04-21 at the earliest; let age for a year (2018-10) or longer for optimum taste!',
        'bottleDistribution': [
            ['1', 'JKi'],
            ['2', 'JKi'],
            ['3', 'CBa'],
            ['4', 'MMa'],
            ['5', 'MBa']
        ],
        'timeline': [
            // date, notes
            ['2017-10-09 1830', 'Must prepared.'],
            ['2017-10-09 1930', 'Carboy airlocked.'],
            ['2017-10-09 2230', 'Very few bubbles; some tiny surface ones but only ~10% coverage.'],
            ['2017-10-10 0030', 'Bubble from yeast every minute or two'],
            ['2017-10-11 1415', 'Bubbles every two to three seconds; moved to warmed surroundings with towel around carboy for light and warmth']
        ],
        'gravReadings': [
            // date, OG, notes
            ['2017-10-09', 1.12, 'Pitched']
        ],
        'images': ['img/f1mustevening.jpg', 'img/mixedmusts.jpg', 'img/musts.jpg', 'img/ingredients.jpg', 'img/bundled.jpg'],
        'tastingNotes': ''
    },
    {
        'batchID': 2,
        'type': 'Semi-Dry Mead',
        'recipeName': 'Jutini-α',
        'bottleRangeMin': 6,
        'bottleRangeMax': 10,
        'size': '1 gal.',
        'notes': 'This is one of my first two meads! This mead uses a stronger yeast that should result in a semi-dry mead. This is a small batch and may or may not come out tasting like battery acid -- as always, longer aging makes for a better mead!

Honeys used: 3lb Crocket Raw Arizona Desert Wildflower honey, .5lb Nature Nate\'s Raw Unfiltered honey. Promised bottles: B6-7 JWK, B8 HLu, B9 MWe, B10 JFe.',
        'openingDate': 'Open 2018-04-21 at the earliest; let age for a year (2018-10) or longer for optimum taste!',
        'bottleDistribution': [
            ['6', 'JKi'],
            ['7', 'JKi'],
            ['8', 'HLu'],
            ['9', 'MWe'],
            ['10', 'JFe']
        ],
        'timeline': [
            // date, notes
            ['2017-10-09 1830', 'Must prepared.'],
            ['2017-10-09 1930', 'Carboy airlocked.'],
            ['2017-10-09 2230', 'Lots of tiny bubbles; 100% surface coverage and about 2mm thick.'],
            ['2017-10-10 0030', 'Creamy layer of top bubbles; bubble through airlock once every 10 seconds. These yeast are HUNGRY!'],
            ['2017-10-11 1415', 'Bubbles every second; moved to warmed surroundings with towel around carboy for light and warmth']
        ],
        'gravReadings': [
            // date, OG, notes
            ['2017-10-09', 1.11, 'Pitched']
        ],
        images: ['img/f2bubbling.gif', 'img/f2mustevening.jpg', 'img/mixedmusts.jpg', 'img/f2must.jpg', 'img/musts.jpg', 'img/ingredients.jpg', 'img/f2yeast.jpg', 'img/bundled.jpg'],
        'tastingNotes': '',
    }
];

var recipes = {
    'Wabozu\'': `Fiery ginger ale

**Volume Produced:** 2 liters

**Brew time:** 2 days

### Ingredients
- 1.5 oz. finely grated fresh ginger (the younger the better; microplane suggested)
- 6 oz. table sugar
- 7.5 cups filtered water
- 1/8 teaspoon (should be around 750mg) champagne yeast (Red Star Premier Blanc formerly known as Pastuer Champagne)
- 2 tablespoons freshly squeezed lemon juice
- StarSan soln.

### Equipment/Misc
- 2L PET bottle (i.e. plastic soda bottle)

### Prep
Clean 2L PET bottle with StarSan soln.

### Recipe
Place ginger, sugar, and 1/2 cup of the water into a saucepan over medium-high heat. Stir until the sugar dissolves. Remove from heat, cover, and allow to steep for 1 hour.

Pour the syrup through a fine strainer, cheese cloth over a bowl, or t-shirt, using pressure to get all the juice out of the mixture. Rapidly chill in an ice bath and stir until the mixture reaches 70 degrees F.

Pour the syrup into a 2L PET bottle and add yeast, lemon juice, and remaining 7 cups of water. Shake vigorously for five minutes. Let sit in a dark place at room temperature.

### Rack'n'Pack
At 2 days, burp and refrigerate bottle. Allow 24 hours for yeast to fall asleep, then siphon off into another StarSan'd 2L soda bottle, leaving the flocculated yeast at the bottom of the first bottle.  Take and record gravity reading if desired. Keep chilled and drink quickly; the carbonation will not last long -- 3 days is safe.`,
    'Jutini\'': `Simple sweet mead

**Voume Produced:** 1 gallon

**Brew time:** 1 month primary, 2 week secondary/stabilization

### Ingredients
- 3.5 lbs honey
- 1 tsp LD Carlson Nutrient (diammonium phosphate/DAP)
- 0.5 tsp LD Carlson Energizer (yeast hulls)
- 5 grams Safale US-05 yeast
- Distilled water to 1 gal.

### Equipment/Misc.
- 1 gal. carboy
- Airlock
- StarSan soln.
- 1 Campden tablet, crushed

### Prep
Clean 1 gal. carboy with StarSan soln. .1oz starsan (.6 tsp) for half gallon. Rinse until suds subside.

### Recipe
Add room temperature honey, then room temperature distilled water until volume of must is 1 gal.

Pour dry yeast into must, and add DAP with yeast hulls.

Seal carboy and shake vigorously for 4 minutes. Take and record gravity reading if desired. Airlock carboy.

Place in dark location at 60 - 75 degrees F.

After two weeks, check for fermentation completion via SG check. Once complete, rack onto crushed campden and let sit for two weeks or until clear, then bottle.`,
    'Jutini-α': `Simple semi-dry mead

**Voume Produced:** 1 gallon

**Brew time:** 1 month primary, 2 week secondary/stabilization

### Ingredients
- 3.5 lbs honey
- 1 tsp LD Carlson Nutrient (diammonium phosphate/DAP)
- 0.5 tsp LD Carlson Energizer (yeast hulls)
- 5 grams Lavlin 71B-1122
- Distilled water to 1 gal.

### Equipment/Misc.
- 1 gal. carboy
- Airlock
- StarSan soln.
- 1 Campden tablet, crushed

### Prep
Clean 1 gal. carboy with StarSan soln. .1oz starsan (.6 tsp) for half gallon. Rinse until suds subside.

### Recipe
Add room temperature honey, then room temperature distilled water until volume of must is 1 gal.

Pour dry yeast into must, and add DAP with yeast hulls.

Seal carboy and shake vigorously for 4 minutes. Take and record gravity reading if desired. Airlock carboy.

Place in dark location at 60 - 75 degrees F.

After two weeks, check for fermentation completion via SG check. Once complete, rack onto crushed campden and let sit for two weeks or until clear, then bottle.`,
    'Soravo\'': `Reaper Capsicumel

**Voume Produced:** 1 gallon

**Brew time:** 1 month primary, 1 month oaking/secondary, 2 weeks stabilization

### Ingredients
- 3 lbs honey
- 1 tsp LD Carlson Nutrient (diammonium phosphate/DAP)
- 0.5 tsp LD Carlson Energizer (yeast hulls)
- 5 grams Lavlin EC-1118
- 4 Carolina Reaper chilis
- 1 Med. toast oak spiral
- Distilled water to 1 gal.

### Equipment/Misc.
- 1 gal. carboy
- Airlock
- StarSan soln.
- 1 Campden tablet, crushed

### Prep
Clean 1 gal. carboy with StarSan soln. .1oz starsan (.6 tsp) for half gallon. Rinse until suds subside.

### Recipe
Add room temperature honey, then room temperature distilled water until volume of must is short of 1 gal. Pour dry yeast into must, and add DAP with yeast hulls.

Seal carboy and shake vigorously for 4 minutes.

Take and record gravity reading if desired.

Simmer chopped and de-seeded chilis in 1/4 cup of water for ten minutes; pour into must through muslin and tie off muslin.

Fill with water to 1 gal. Airlock carboy.

Place in dark location at 60 - 75 degrees F.

After 1 month, rack off peppers and into new vessel with oak spiral.

After another month, rack off oak and add crushed Campden tablet. Let sit for two weeks or until clear, then bottle.`,
    'Duduhu\'': `Ginger metheglin

**Voume Produced:** 1 gallon

**Brew time:** 1 month primary, 2 month (or to taste) secondary, 2 week stabilization

### Ingredients
- 3.5 lbs honey
- 1 tsp LD Carlson Nutrient (diammonium phosphate/DAP)
- 0.5 tsp LD Carlson Energizer (yeast hulls)
- 1 lb raw ginger, peeled and cubed
- 5 grams Lavlin 71B-1122
- Distilled water to 1 gal.

### Equipment/Misc.
- 1 gal. carboy
- Airlock
- StarSan soln.
- 1 Campden tablet, crushed

### Prep
Clean 1 gal. carboy with StarSan soln. .1oz starsan (.6 tsp) for half gallon. Rinse until suds subside.

### Recipe
Add room temperature honey, then room temperature distilled water until volume of must is shy of 1 gal.

Pour dry yeast into must, and add DAP with yeast hulls.

Seal carboy and shake vigorously for 4 minutes. Take and record gravity reading if desired.

Fill to 1 gal., put into carboy, and airlock.Place in dark location at 60 - 75 degrees F.

After one month, rack onto peeled and cubed ginger.

After two months (or to taste) on ginger, rack onto crushed campden and let sit for two weeks or until clear, then bottle.`
};
<|endoftext|>