<commit_before>// Copyright 2013 Andreas Koch. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package themes

var defaultTheme *Theme

func init() {

	const screenCss = `
html {
    font-size: 100%;
    overflow-y: scroll;
    -webkit-text-size-adjust: 100%;
    -ms-text-size-adjust: 100%;
}

body {
    color: #444;
    font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', "Hiragino Sans GB", "STXihei", "微软雅黑", serif;
    font-size: 12px;
    line-height: 1.5em;
    background: #fefefe;
    width: 75%;
    margin: 10px auto;
    padding: 1em;
    outline: 1300px solid #FAFAFA;
}

a {
    color: #0645ad;
    text-decoration: none;
}

a:visited {
    color: #0b0080;
}

a:hover {
    color: #06e;
}

a:active {
    color: #faa700;
}

a:focus {
    outline: thin dotted;
}

a:hover, a:active {
    outline: 0;
}

span.backtick {
    border: 1px solid #EAEAEA;
    border-radius: 3px;
    background: #F8F8F8;
    padding: 0 3px 0 3px;
}

::-moz-selection {
    background: rgba(255,255,0,0.3);
    color: #000;
}

::selection {
    background: rgba(255,255,0,0.3);
    color: #000;
}

a::-moz-selection {
    background: rgba(255,255,0,0.3);
    color: #0645ad;
}

a::selection {
    background: rgba(255,255,0,0.3);
    color: #0645ad;
}

p {
    margin: 1em 0;
}

img {
    max-width: 100%;
}

h1,h2,h3,h4,h5,h6 {
    font-weight: normal;
    color: #111;
    line-height: 1em;
}

h4,h5,h6 {
    font-weight: bold;
}

h1 {
    font-size: 2.5em;
}

h2 {
    font-size: 2em;
    border-bottom: 1px solid silver;
    padding-bottom: 5px;
}

h3 {
    font-size: 1.5em;
}

h4 {
    font-size: 1.2em;
}

h5 {
    font-size: 1em;
}

h6 {
    font-size: 0.9em;
}

blockquote {
    color: #666666;
    margin: 0;
    padding-left: 3em;
    border-left: 0.5em #EEE solid;
}

hr {
    display: block;
    height: 2px;
    border: 0;
    border-top: 1px solid #aaa;
    border-bottom: 1px solid #eee;
    margin: 1em 0;
    padding: 0;
}

pre , code, kbd, samp {
    color: #000;
    font-family: monospace;
    font-size: 0.88em;
    border-radius: 3px;
    background-color: #F8F8F8;
    border: 1px solid #CCC;
}

pre {
    white-space: pre;
    white-space: pre-wrap;
    word-wrap: break-word;
    padding: 5px 12px;
}

pre code {
    border: 0px !important;
    padding: 0;
}

code {
    padding: 0 3px 0 3px;
}

b, strong {
    font-weight: bold;
}

dfn {
    font-style: italic;
}

ins {
    background: #ff9;
    color: #000;
    text-decoration: none;
}

mark {
    background: #ff0;
    color: #000;
    font-style: italic;
    font-weight: bold;
}

sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
}

sup {
    top: -0.5em;
}

sub {
    bottom: -0.25em;
}

ul, ol {
    margin: 1em 0;
    padding: 0 0 0 2em;
}

li p:last-child {
    margin: 0;
}

dd {
    margin: 0 0 0 2em;
}

img {
    border: 0;
    -ms-interpolation-mode: bicubic;
    vertical-align: middle;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}

td {
    vertical-align: top;
}

.imagegallery>h1 {
    font-size: 1.2em;
}

.imagegallery ol {
    list-style: none;
    margin-left: 0;
}

.collection>h1 {
    font-size: 1.2em;
}

.collection>ol {
    list-style: none;
    padding: 0;
    margin: 0 0 0 15px;
}

.collection>ol>li {
    display: block;
    margin: 0 0 15px 0;
}

.collection>ol>li>h2 {
    font-size: 1.0em;
    font-weight: bold;
    margin: 0;
}

.csv
{
    font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
    font-size: 1.0em;
    margin: 45px;
    text-align: left;
    border-collapse: collapse;
    border: 1px solid #69c;
}
.csv thead
{
    padding: 12px 17px 12px 17px;
    font-weight: normal;
    font-size: 1.2em;
    color: #039;
    border-bottom: 1px dashed #69c;
}
.csv td
{
    padding: 7px 17px 7px 17px;
    color: #669;
}
.csv tbody tr:hover td
{
    color: #339;
    background: #d0dafd;
}

@media only screen and (min-width: 480px) {
    body {
        font-size: 14px;
        width: 95%
    };
}

@media only screen and (min-width: 768px) {
    body {
        font-size: 16px;
        width: 95%
    };
}

@media only screen and (min-width: 1024px) {
    body {
        font-size: 16px;
        width: 75%
    };
}

@media print {
    * {
        background: transparent !important;
        color: black !important;
        filter: none !important;
        -ms-filter: none !important;
    }

    body {
        font-size: 12pt;
        max-width: 100%;
        width: 100%
        outline: none;
    }

    a, a:visited {
        text-decoration: underline;
    }

    hr {
        height: 1px;
        border: 0;
        border-bottom: 1px solid black;
    }

    a[href]:after {
        content: " (" attr(href) ")";
    }

    abbr[title]:after {
        content: " (" attr(title) ")";
    }

    .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after {
        content: "";
    }

    pre, blockquote {
        border: 1px solid #999;
        padding-right: 1em;
        page-break-inside: avoid;
    }

    tr, img {
        page-break-inside: avoid;
    }

    img {
        max-width: 95% !important;
    }

    @page :left {
        margin: 15mm 20mm 15mm 10mm;
    }

    @page :right {
        margin: 15mm 10mm 15mm 20mm;
    }

    p, h2, h3 {
        orphans: 3;
        widows: 3;
    }

    h2, h3 {
        page-break-after: avoid;
    };
}`

	const autoupdateJs = `
$(function() {

    // check if websockets are supported
    if (!window["WebSocket"]) {
        console.log("Your browser does not support WebSockets.");
        return;
    }

    /**
     * Get the currently opened web route
     * @return string The currently opened web route (e.g. "/documents/Sample Document/index.html")
     */
    var getCurrentRoute = function() {
        var url = document.location.pathname;
        return url.replace(/^\/+/, "");
    };

    /**
     * Get the Url for the web socket connection
     * @return string The url for the web socket connection (e.g. "ws://example.com:8080/ws")
     */
    var getWebSocketUrl = function() {
        routeParameter = "route=" + getCurrentRoute();
        host = document.location.host;
        webSocketHandler = "/ws";
        websocketUrl = "ws://" + host + webSocketHandler + "?" + routeParameter;
        return websocketUrl;
    };

    var connection = new WebSocket(getWebSocketUrl());

    connection.onclose = function(evt) {
        console.log("Connection closed.");
    };

    connection.onopen = function() {
        console.log("Connection established.")
    };

    connection.onmessage = function(evt) {

        // validate event data
        if (typeof(evt) !== 'object' || typeof(evt.data) !== 'string') {
            console.log("Invalid data from server.");
            return;
        }

        // unwrap the message
        message = JSON.parse(evt.data);

        // check if all required fields are present
        if (message === null || typeof(message) !== 'object' || typeof(message.route) !== 'string' || message.model === null || typeof(message.model) !== 'object') {
            console.log("Invalid response format.", message);
            return;
        }

        // check if update is applicable for the current route
        if (message.route !== getCurrentRoute()) {
            return;
        }

        // check the model structure
        var model = message.model;
        if (typeof(model.content) !== 'string' || typeof(model.description) !== 'string' || typeof(model.title) !== 'string') {
            console.log("Cannot update the view with the given model object. Missing some required fields.", model);
            return;
        }

        // update the title
        $('title').html(model.title);
        $('.title').html(model.title);

        // update the description
        $('.description').html(model.description);

        // update the content
        $('.content').html(model.content);

        // update sub entries (if available)
        if (model.subEntries === null || typeof(model.subEntries) !== 'object') {
            return;
        }

        var entries = model.subEntries;
        var existingEntries = $(".subentries>.subentry");
        var numberOfExistingEntries = existingEntries.length;
        var numberOfNewEntries = entries.length;

        var fallbackEntryTemplate = "

";

        for (var i = 0; i < numberOfNewEntries; i++) {
            var index = i + 1;
            var newEntry = entries[i];

            // get the item to update
            var entryToUpdate;
            if (index <= numberOfExistingEntries) {

                // use an existing item
                entryToUpdate = existingEntries[i];

            } else {

                // create a new item
                var entryTemplate = fallbackEntryTemplate;
                if (numberOfExistingEntries > 0) {

                    // use the first entry for the template
                    entryTemplate = existingEntries[0].html();

                }

                // append the template
                entryToUpdate = $(entryTemplate);
                $(".subentries").append(entryToUpdate);
            }

            // update the title text
            $(entryToUpdate).find(".subentry-link:first").html(newEntry.title);

            // update the title link
            $(entryToUpdate).find(".subentry-link:first").attr("href", newEntry.relativeRoute);

            // update the description
            $(entryToUpdate).find(".subentry-description:first").html(newEntry.description);
        }

    };
});`

	defaultTheme = &Theme{
		Name: "default",
		Files: []*ThemeFile{
			&ThemeFile{
				Filename: "screen.css",
				Content:  screenCss,
			},
			&ThemeFile{
				Filename: "autoupdate.js",
				Content:  autoupdateJs,
			},
		},
	}
}
<commit_msg>Make sure the current route is decoded before comparing it with the route sent over the socket connection<commit_after>// Copyright 2013 Andreas Koch. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package themes

var defaultTheme *Theme

func init() {

	const screenCss = `
html {
    font-size: 100%;
    overflow-y: scroll;
    -webkit-text-size-adjust: 100%;
    -ms-text-size-adjust: 100%;
}

body {
    color: #444;
    font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', "Hiragino Sans GB", "STXihei", "微软雅黑", serif;
    font-size: 12px;
    line-height: 1.5em;
    background: #fefefe;
    width: 75%;
    margin: 10px auto;
    padding: 1em;
    outline: 1300px solid #FAFAFA;
}

a {
    color: #0645ad;
    text-decoration: none;
}

a:visited {
    color: #0b0080;
}

a:hover {
    color: #06e;
}

a:active {
    color: #faa700;
}

a:focus {
    outline: thin dotted;
}

a:hover, a:active {
    outline: 0;
}

span.backtick {
    border: 1px solid #EAEAEA;
    border-radius: 3px;
    background: #F8F8F8;
    padding: 0 3px 0 3px;
}

::-moz-selection {
    background: rgba(255,255,0,0.3);
    color: #000;
}

::selection {
    background: rgba(255,255,0,0.3);
    color: #000;
}

a::-moz-selection {
    background: rgba(255,255,0,0.3);
    color: #0645ad;
}

a::selection {
    background: rgba(255,255,0,0.3);
    color: #0645ad;
}

p {
    margin: 1em 0;
}

img {
    max-width: 100%;
}

h1,h2,h3,h4,h5,h6 {
    font-weight: normal;
    color: #111;
    line-height: 1em;
}

h4,h5,h6 {
    font-weight: bold;
}

h1 {
    font-size: 2.5em;
}

h2 {
    font-size: 2em;
    border-bottom: 1px solid silver;
    padding-bottom: 5px;
}

h3 {
    font-size: 1.5em;
}

h4 {
    font-size: 1.2em;
}

h5 {
    font-size: 1em;
}

h6 {
    font-size: 0.9em;
}

blockquote {
    color: #666666;
    margin: 0;
    padding-left: 3em;
    border-left: 0.5em #EEE solid;
}

hr {
    display: block;
    height: 2px;
    border: 0;
    border-top: 1px solid #aaa;
    border-bottom: 1px solid #eee;
    margin: 1em 0;
    padding: 0;
}

pre , code, kbd, samp {
    color: #000;
    font-family: monospace;
    font-size: 0.88em;
    border-radius: 3px;
    background-color: #F8F8F8;
    border: 1px solid #CCC;
}

pre {
    white-space: pre;
    white-space: pre-wrap;
    word-wrap: break-word;
    padding: 5px 12px;
}

pre code {
    border: 0px !important;
    padding: 0;
}

code {
    padding: 0 3px 0 3px;
}

b, strong {
    font-weight: bold;
}

dfn {
    font-style: italic;
}

ins {
    background: #ff9;
    color: #000;
    text-decoration: none;
}

mark {
    background: #ff0;
    color: #000;
    font-style: italic;
    font-weight: bold;
}

sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
}

sup {
    top: -0.5em;
}

sub {
    bottom: -0.25em;
}

ul, ol {
    margin: 1em 0;
    padding: 0 0 0 2em;
}

li p:last-child {
    margin: 0;
}

dd {
    margin: 0 0 0 2em;
}

img {
    border: 0;
    -ms-interpolation-mode: bicubic;
    vertical-align: middle;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}

td {
    vertical-align: top;
}

.imagegallery>h1 {
    font-size: 1.2em;
}

.imagegallery ol {
    list-style: none;
    margin-left: 0;
}

.collection>h1 {
    font-size: 1.2em;
}

.collection>ol {
    list-style: none;
    padding: 0;
    margin: 0 0 0 15px;
}

.collection>ol>li {
    display: block;
    margin: 0 0 15px 0;
}

.collection>ol>li>h2 {
    font-size: 1.0em;
    font-weight: bold;
    margin: 0;
}

.csv
{
    font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
    font-size: 1.0em;
    margin: 45px;
    text-align: left;
    border-collapse: collapse;
    border: 1px solid #69c;
}
.csv thead
{
    padding: 12px 17px 12px 17px;
    font-weight: normal;
    font-size: 1.2em;
    color: #039;
    border-bottom: 1px dashed #69c;
}
.csv td
{
    padding: 7px 17px 7px 17px;
    color: #669;
}
.csv tbody tr:hover td
{
    color: #339;
    background: #d0dafd;
}

@media only screen and (min-width: 480px) {
    body {
        font-size: 14px;
        width: 95%
    };
}

@media only screen and (min-width: 768px) {
    body {
        font-size: 16px;
        width: 95%
    };
}

@media only screen and (min-width: 1024px) {
    body {
        font-size: 16px;
        width: 75%
    };
}

@media print {
    * {
        background: transparent !important;
        color: black !important;
        filter: none !important;
        -ms-filter: none !important;
    }

    body {
        font-size: 12pt;
        max-width: 100%;
        width: 100%
        outline: none;
    }

    a, a:visited {
        text-decoration: underline;
    }

    hr {
        height: 1px;
        border: 0;
        border-bottom: 1px solid black;
    }

    a[href]:after {
        content: " (" attr(href) ")";
    }

    abbr[title]:after {
        content: " (" attr(title) ")";
    }

    .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after {
        content: "";
    }

    pre, blockquote {
        border: 1px solid #999;
        padding-right: 1em;
        page-break-inside: avoid;
    }

    tr, img {
        page-break-inside: avoid;
    }

    img {
        max-width: 95% !important;
    }

    @page :left {
        margin: 15mm 20mm 15mm 10mm;
    }

    @page :right {
        margin: 15mm 10mm 15mm 20mm;
    }

    p, h2, h3 {
        orphans: 3;
        widows: 3;
    }

    h2, h3 {
        page-break-after: avoid;
    };
}`

	const autoupdateJs = `
$(function() {

    // check if websockets are supported
    if (!window["WebSocket"]) {
        console.log("Your browser does not support WebSockets.");
        return;
    }

    /**
     * Get the currently opened web route
     * @return string The currently opened web route (e.g. "/documents/Sample Document/index.html")
     */
    var getCurrentRoute = function() {
        var url = document.location.pathname;
        return decodeURI(url.replace(/^\/+/, ""));
    };

    /**
     * Get the Url for the web socket connection
     * @return string The url for the web socket connection (e.g. "ws://example.com:8080/ws")
     */
    var getWebSocketUrl = function() {
        routeParameter = "route=" + getCurrentRoute();
        host = document.location.host;
        webSocketHandler = "/ws";
        websocketUrl = "ws://" + host + webSocketHandler + "?" + routeParameter;
        return websocketUrl;
    };

    var connection = new WebSocket(getWebSocketUrl());

    connection.onclose = function(evt) {
        console.log("Connection closed.");
    };

    connection.onopen = function() {
        console.log("Connection established.")
    };

    connection.onmessage = function(evt) {

        // validate event data
        if (typeof(evt) !== 'object' || typeof(evt.data) !== 'string') {
            console.log("Invalid data from server.");
            return;
        }

        // unwrap the message
        message = JSON.parse(evt.data);

        // check if all required fields are present
        if (message === null || typeof(message) !== 'object' || typeof(message.route) !== 'string' || message.model === null || typeof(message.model) !== 'object') {
            console.log("Invalid response format.", message);
            return;
        }

        // check if update is applicable for the current route
        if (message.route !== getCurrentRoute()) {
            return;
        }

        // check the model structure
        var model = message.model;
        if (typeof(model.content) !== 'string' || typeof(model.description) !== 'string' || typeof(model.title) !== 'string') {
            console.log("Cannot update the view with the given model object. Missing some required fields.", model);
            return;
        }

        // update the title
        $('title').html(model.title);
        $('.title').html(model.title);

        // update the description
        $('.description').html(model.description);

        // update the content
        $('.content').html(model.content);

        // update sub entries (if available)
        if (model.subEntries === null || typeof(model.subEntries) !== 'object') {
            return;
        }

        var entries = model.subEntries;
        var existingEntries = $(".subentries>.subentry");
        var numberOfExistingEntries = existingEntries.length;
        var numberOfNewEntries = entries.length;

        var fallbackEntryTemplate = "

";

        for (var i = 0; i < numberOfNewEntries; i++) {
            var index = i + 1;
            var newEntry = entries[i];

            // get the item to update
            var entryToUpdate;
            if (index <= numberOfExistingEntries) {

                // use an existing item
                entryToUpdate = existingEntries[i];

            } else {

                // create a new item
                var entryTemplate = fallbackEntryTemplate;
                if (numberOfExistingEntries > 0) {

                    // use the first entry for the template
                    entryTemplate = existingEntries[0].html();

                }

                // append the template
                entryToUpdate = $(entryTemplate);
                $(".subentries").append(entryToUpdate);
            }

            // update the title text
            $(entryToUpdate).find(".subentry-link:first").html(newEntry.title);

            // update the title link
            $(entryToUpdate).find(".subentry-link:first").attr("href", newEntry.relativeRoute);

            // update the description
            $(entryToUpdate).find(".subentry-description:first").html(newEntry.description);
        }

    };
});`

	defaultTheme = &Theme{
		Name: "default",
		Files: []*ThemeFile{
			&ThemeFile{
				Filename: "screen.css",
				Content:  screenCss,
			},
			&ThemeFile{
				Filename: "autoupdate.js",
				Content:  autoupdateJs,
			},
		},
	}
}
<|endoftext|>


<commit_before>package themes

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"github.com/nlopes/slack"
	"github.com/tuna-timer/tuna-timer-api/models"
	"github.com/tuna-timer/tuna-timer-api/utils"
	"time"
)

// DefaultSlackMessageTheme - the basic UI theme for messages that go back from us to Slack users
type DefaultSlackMessageTheme struct {
	themeConfig
	ctx context.Context
}

var defaultThemeConfig = themeConfig{
	MarkdownEnabledFor:     []string{"text", "pretext"},
	SummaryAttachmentColor: "#000000",
	FooterIcon:             "http://icons.iconarchive.com/icons/martin-berube/flat-animal/48/tuna-icon.png",
	StartCommandThumbURL:   "/assets/themes/default/ic_current.png",
	StartCommandColor:      "F5A623",
	StopCommandThumbURL:    "/assets/themes/default/ic_completed.png",
	StopCommandColor:       "#4A90E2",
	StatusCommandThumbURL:  "/assets/themes/default/ic_status.png",
	StatusCommandColor:     "#9B9B9B",
}

func NewDefaultSlackMessageTheme(ctx context.Context) *DefaultSlackMessageTheme {
	return &DefaultSlackMessageTheme{
		themeConfig: defaultThemeConfig,
		ctx:         ctx,
	}
}

func (t *DefaultSlackMessageTheme) FormatStatusCommand(data *models.StatusCommandReport) string {

	tpl := slackThemeTemplate{
		Text:        fmt.Sprintf("Your status for %s", data.PeriodName),
		Attachments: []slack.Attachment{},
	}

	summaryAttachmentVisible := len(data.Tasks) > 0 || data.AlreadyStartedTimer != nil

	if len(data.Tasks) > 0 {
		statusAttachment := t.defaultAttachment()
		statusAttachment.ThumbURL = t.asset(t.StopCommandThumbURL)
		statusAttachment.Color = t.StopCommandColor

		statusAttachment.Footer = "<http://www.foo.com|Open in Application>"
		var buffer bytes.Buffer
		for _, task := range data.Tasks {
			if data.AlreadyStartedTimer == nil || data.AlreadyStartedTimer.TaskName != task.Name {
				projectLink := ""
				if data.Project.ID.Hex() != task.ProjectID {
					projectLink = fmt.Sprintf(", %s ", t.channelLink(task.ProjectExternalID, task.ProjectExternalName))
				}
				buffer.WriteString(fmt.Sprintf("•  *%s*%s %s\n",
					utils.FormatDuration(time.Duration(int64(task.Minutes)*int64(time.Minute))),
					projectLink,
					task.Name))
			}
		}
		statusAttachment.AuthorName = "Completed:"
		statusAttachment.Text = buffer.String()
		tpl.Attachments = append(tpl.Attachments, statusAttachment)
	}

	if data.AlreadyStartedTimer != nil {
		sa := t.attachmentForCurrentTask(data.AlreadyStartedTimer, data.AlreadyStartedTimerTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	if summaryAttachmentVisible {
		tpl.Attachments = append(tpl.Attachments, t.summaryAttachment(data.PeriodName, data.UserTotalForPeriod))
	} else {
		tpl.Text = fmt.Sprintf("You have no tasks completed %s", data.PeriodName)
	}

	result, err := json.Marshal(tpl)
	if err != nil {
		// todo return { "text": err.String() }
	}

	return string(result)
}

func (t *DefaultSlackMessageTheme) FormatStopCommand(data *models.StopCommandReport) string {
	tpl := slackThemeTemplate{
		Attachments: []slack.Attachment{},
	}

	if data.StoppedTimer != nil {
		sa := t.attachmentForStoppedTask(data.StoppedTimer, data.StoppedTaskTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	tpl.Attachments = append(tpl.Attachments, t.summaryAttachment("today", data.UserTotalForToday))

	result, err := json.Marshal(tpl)
	if err != nil {
		// todo return { "text": err.String() }
	}

	return string(result)
}

func (t *DefaultSlackMessageTheme) FormatStartCommand(data *models.StartCommandReport) string {
	tpl := slackThemeTemplate{
		Attachments: []slack.Attachment{},
	}

	if data.StoppedTimer != nil {
		sa := t.attachmentForStoppedTask(data.StoppedTimer, data.StoppedTaskTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	if data.StartedTimer != nil {
		sa := t.attachmentForNewTask(data.StartedTimer, data.StartedTaskTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	if data.AlreadyStartedTimer != nil {
		sa := t.attachmentForNewTask(data.AlreadyStartedTimer, data.AlreadyStartedTimerTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	tpl.Attachments = append(tpl.Attachments, t.summaryAttachment("today", data.UserTotalForToday))

	result, err := json.Marshal(tpl)
	if err != nil {
		// todo return { "text": err.String() }
	}

	return string(result)
}

func (t *DefaultSlackMessageTheme) attachmentForNewTask(timer *models.Timer, taskTotalForToday int) slack.Attachment {
	sa := t.defaultAttachment()
	sa.Text = fmt.Sprintf("•  *%s*  %s\n", utils.FormatDuration(time.Duration(int64(taskTotalForToday)*int64(time.Minute))), timer.TaskName)
	sa.ThumbURL = t.asset(t.StartCommandThumbURL)
	sa.Color = t.StartCommandColor
	sa.AuthorName = "Started:"

	sa.Footer = fmt.Sprintf(
		"Project: #%s > Task ID: %s > <http://www.google.com|Edit in Application>", t.channelLinkForTimer(timer), timer.TaskHash)

	return sa
}

func (t *DefaultSlackMessageTheme) attachmentForCurrentTask(timer *models.Timer, totalForToday int) slack.Attachment {
	sa := t.defaultAttachment()
	sa.Text = fmt.Sprintf("•  *%s*  %s\n", utils.FormatDuration(time.Duration(int64(totalForToday)*int64(time.Minute))), timer.TaskName)
	sa.ThumbURL = t.asset(t.StartCommandThumbURL)
	sa.Color = t.StartCommandColor
	sa.AuthorName = "Current:"

	sa.Footer = fmt.Sprintf(
		"Project: %s > Task ID: %s > <http://www.google.com|Open in Application>", t.channelLinkForTimer(timer), timer.TaskHash)

	sa.Fields = []slack.AttachmentField{}
	return sa
}

func (t *DefaultSlackMessageTheme) attachmentForStoppedTask(timer *models.Timer, totalForToday int) slack.Attachment {
	sa := t.defaultAttachment()
	sa.AuthorName = "Completed:"

	sa.Text = fmt.Sprintf("•  *%s*  %s\n", utils.FormatDuration(time.Duration(int64(totalForToday)*int64(time.Minute))), timer.TaskName)
	sa.ThumbURL = t.asset(t.StopCommandThumbURL)
	sa.Color = t.StopCommandColor

	sa.Footer = fmt.Sprintf(
		"Project: %s > Task ID: %s > <http://www.google.com|Open in Application>", t.channelLinkForTimer(timer), timer.TaskHash)

	sa.Fields = []slack.AttachmentField{}
	return sa
}

func (t *DefaultSlackMessageTheme) summaryAttachment(period string, minutes int) slack.Attachment {
	result := slack.Attachment{}
	result.Text = fmt.Sprintf("*Your total for %s is %s*",
		period,
		utils.FormatDuration(time.Duration(int64(minutes)*int64(time.Minute))))

	result.Color = t.SummaryAttachmentColor
	result.MarkdownIn = t.MarkdownEnabledFor
	return result
}

func (t *DefaultSlackMessageTheme) defaultAttachment() slack.Attachment {
	result := slack.Attachment{}
	result.MarkdownIn = t.MarkdownEnabledFor
	return result
}

func (t *DefaultSlackMessageTheme) asset(assetPath string) string {
	return utils.GetSelfBaseURLFromContext(t.ctx) + assetPath
}

func (t *DefaultSlackMessageTheme) channelLinkForTimer(timer *models.Timer) string {
	return t.channelLink(timer.ProjectExternalID, timer.ProjectExternalName)
}

func (t *DefaultSlackMessageTheme) channelLink(channelID, channelName string) string {
	return fmt.Sprintf("<#%s|%s>  ", channelID, channelName)
}
<commit_msg>removes minor typo<commit_after>package themes

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"github.com/nlopes/slack"
	"github.com/tuna-timer/tuna-timer-api/models"
	"github.com/tuna-timer/tuna-timer-api/utils"
	"time"
)

// DefaultSlackMessageTheme - the basic UI theme for messages that go back from us to Slack users
type DefaultSlackMessageTheme struct {
	themeConfig
	ctx context.Context
}

var defaultThemeConfig = themeConfig{
	MarkdownEnabledFor:     []string{"text", "pretext"},
	SummaryAttachmentColor: "#000000",
	FooterIcon:             "http://icons.iconarchive.com/icons/martin-berube/flat-animal/48/tuna-icon.png",
	StartCommandThumbURL:   "/assets/themes/default/ic_current.png",
	StartCommandColor:      "F5A623",
	StopCommandThumbURL:    "/assets/themes/default/ic_completed.png",
	StopCommandColor:       "#4A90E2",
	StatusCommandThumbURL:  "/assets/themes/default/ic_status.png",
	StatusCommandColor:     "#9B9B9B",
}

func NewDefaultSlackMessageTheme(ctx context.Context) *DefaultSlackMessageTheme {
	return &DefaultSlackMessageTheme{
		themeConfig: defaultThemeConfig,
		ctx:         ctx,
	}
}

func (t *DefaultSlackMessageTheme) FormatStatusCommand(data *models.StatusCommandReport) string {

	tpl := slackThemeTemplate{
		Text:        fmt.Sprintf("Your status for %s", data.PeriodName),
		Attachments: []slack.Attachment{},
	}

	summaryAttachmentVisible := len(data.Tasks) > 0 || data.AlreadyStartedTimer != nil

	if len(data.Tasks) > 0 {
		statusAttachment := t.defaultAttachment()
		statusAttachment.ThumbURL = t.asset(t.StopCommandThumbURL)
		statusAttachment.Color = t.StopCommandColor

		statusAttachment.Footer = "<http://www.foo.com|Open in Application>"
		var buffer bytes.Buffer
		for _, task := range data.Tasks {
			if data.AlreadyStartedTimer == nil || data.AlreadyStartedTimer.TaskName != task.Name {
				projectLink := ""
				if data.Project.ID.Hex() != task.ProjectID {
					projectLink = fmt.Sprintf(", %s ", t.channelLink(task.ProjectExternalID, task.ProjectExternalName))
				}
				buffer.WriteString(fmt.Sprintf("•  *%s*%s %s\n",
					utils.FormatDuration(time.Duration(int64(task.Minutes)*int64(time.Minute))),
					projectLink,
					task.Name))
			}
		}
		statusAttachment.AuthorName = "Completed:"
		statusAttachment.Text = buffer.String()
		tpl.Attachments = append(tpl.Attachments, statusAttachment)
	}

	if data.AlreadyStartedTimer != nil {
		sa := t.attachmentForCurrentTask(data.AlreadyStartedTimer, data.AlreadyStartedTimerTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	if summaryAttachmentVisible {
		tpl.Attachments = append(tpl.Attachments, t.summaryAttachment(data.PeriodName, data.UserTotalForPeriod))
	} else {
		tpl.Text = fmt.Sprintf("You have no tasks completed %s", data.PeriodName)
	}

	result, err := json.Marshal(tpl)
	if err != nil {
		// todo return { "text": err.String() }
	}

	return string(result)
}

func (t *DefaultSlackMessageTheme) FormatStopCommand(data *models.StopCommandReport) string {
	tpl := slackThemeTemplate{
		Attachments: []slack.Attachment{},
	}

	if data.StoppedTimer != nil {
		sa := t.attachmentForStoppedTask(data.StoppedTimer, data.StoppedTaskTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	tpl.Attachments = append(tpl.Attachments, t.summaryAttachment("today", data.UserTotalForToday))

	result, err := json.Marshal(tpl)
	if err != nil {
		// todo return { "text": err.String() }
	}

	return string(result)
}

func (t *DefaultSlackMessageTheme) FormatStartCommand(data *models.StartCommandReport) string {
	tpl := slackThemeTemplate{
		Attachments: []slack.Attachment{},
	}

	if data.StoppedTimer != nil {
		sa := t.attachmentForStoppedTask(data.StoppedTimer, data.StoppedTaskTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	if data.StartedTimer != nil {
		sa := t.attachmentForNewTask(data.StartedTimer, data.StartedTaskTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	if data.AlreadyStartedTimer != nil {
		sa := t.attachmentForNewTask(data.AlreadyStartedTimer, data.AlreadyStartedTimerTotalForToday)
		tpl.Attachments = append(tpl.Attachments, sa)
	}

	tpl.Attachments = append(tpl.Attachments, t.summaryAttachment("today", data.UserTotalForToday))

	result, err := json.Marshal(tpl)
	if err != nil {
		// todo return { "text": err.String() }
	}

	return string(result)
}

func (t *DefaultSlackMessageTheme) attachmentForNewTask(timer *models.Timer, taskTotalForToday int) slack.Attachment {
	sa := t.defaultAttachment()
	sa.Text = fmt.Sprintf("•  *%s*  %s\n", utils.FormatDuration(time.Duration(int64(taskTotalForToday)*int64(time.Minute))), timer.TaskName)
	sa.ThumbURL = t.asset(t.StartCommandThumbURL)
	sa.Color = t.StartCommandColor
	sa.AuthorName = "Started:"

	sa.Footer = fmt.Sprintf(
		"Project: %s > Task ID: %s > <http://www.google.com|Edit in Application>", t.channelLinkForTimer(timer), timer.TaskHash)

	return sa
}

func (t *DefaultSlackMessageTheme) attachmentForCurrentTask(timer *models.Timer, totalForToday int) slack.Attachment {
	sa := t.defaultAttachment()
	sa.Text = fmt.Sprintf("•  *%s*  %s\n", utils.FormatDuration(time.Duration(int64(totalForToday)*int64(time.Minute))), timer.TaskName)
	sa.ThumbURL = t.asset(t.StartCommandThumbURL)
	sa.Color = t.StartCommandColor
	sa.AuthorName = "Current:"

	sa.Footer = fmt.Sprintf(
		"Project: %s > Task ID: %s > <http://www.google.com|Open in Application>", t.channelLinkForTimer(timer), timer.TaskHash)

	sa.Fields = []slack.AttachmentField{}
	return sa
}

func (t *DefaultSlackMessageTheme) attachmentForStoppedTask(timer *models.Timer, totalForToday int) slack.Attachment {
	sa := t.defaultAttachment()
	sa.AuthorName = "Completed:"

	sa.Text = fmt.Sprintf("•  *%s*  %s\n", utils.FormatDuration(time.Duration(int64(totalForToday)*int64(time.Minute))), timer.TaskName)
	sa.ThumbURL = t.asset(t.StopCommandThumbURL)
	sa.Color = t.StopCommandColor

	sa.Footer = fmt.Sprintf(
		"Project: %s > Task ID: %s > <http://www.google.com|Open in Application>", t.channelLinkForTimer(timer), timer.TaskHash)

	sa.Fields = []slack.AttachmentField{}
	return sa
}

func (t *DefaultSlackMessageTheme) summaryAttachment(period string, minutes int) slack.Attachment {
	result := slack.Attachment{}
	result.Text = fmt.Sprintf("*Your total for %s is %s*",
		period,
		utils.FormatDuration(time.Duration(int64(minutes)*int64(time.Minute))))

	result.Color = t.SummaryAttachmentColor
	result.MarkdownIn = t.MarkdownEnabledFor
	return result
}

func (t *DefaultSlackMessageTheme) defaultAttachment() slack.Attachment {
	result := slack.Attachment{}
	result.MarkdownIn = t.MarkdownEnabledFor
	return result
}

func (t *DefaultSlackMessageTheme) asset(assetPath string) string {
	return utils.GetSelfBaseURLFromContext(t.ctx) + assetPath
}

func (t *DefaultSlackMessageTheme) channelLinkForTimer(timer *models.Timer) string {
	return t.channelLink(timer.ProjectExternalID, timer.ProjectExternalName)
}

func (t *DefaultSlackMessageTheme) channelLink(channelID, channelName string) string {
	return fmt.Sprintf("<#%s|%s>  ", channelID, channelName)
}
<|endoftext|>


<commit_before>// Copyright 2013 Aaron Jacobs. All Rights Reserved.
// Author: aaronjjacobs@gmail.com (Aaron Jacobs)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cache_test

import (
	"fmt"
	"github.com/jacobsa/comeback/cache"
	. "github.com/jacobsa/oglematchers"
	. "github.com/jacobsa/ogletest"
	"runtime"
	"sync"
	"testing"
)

const cacheCapacity = 3

func TestLruCache(t *testing.T) { RunTests(t) }

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

type LruCacheTest struct {
	c cache.Cache
}

func init() { RegisterTestSuite(&LruCacheTest{}) }

func (t *LruCacheTest) SetUp(i *TestInfo) {
	t.c = cache.NewLruCache(cacheCapacity)
}

////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////

func (t *LruCacheTest) Empty() {
	ExpectEq(nil, t.c.LookUp(""))
	ExpectEq(nil, t.c.LookUp("taco"))
}

func (t *LruCacheTest) InsertNilValue() {
	ExpectThat(
		func() { t.c.Insert("taco", nil) },
		Panics(HasSubstr("nil value")),
	)
}

func (t *LruCacheTest) LookUpUnknownKey() {
	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)

	ExpectEq(nil, t.c.LookUp(""))
	ExpectEq(nil, t.c.LookUp("enchilada"))
}

func (t *LruCacheTest) FillUpToCapacity() {
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)
	t.c.Insert("enchilada", []byte{0x23, 0x29})

	ExpectEq(17, t.c.LookUp("burrito"))
	ExpectEq(19, t.c.LookUp("taco"))
	ExpectThat(t.c.LookUp("enchilada"), DeepEquals([]byte{0x23, 0x29}))
}

func (t *LruCacheTest) ExpiresLeastRecentlyUsed() {
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)  // Least recent
	t.c.Insert("enchilada", 23)  // Second most recent
	AssertEq(17, t.c.LookUp("burrito"))  // Most recent

	// Insert another.
	t.c.Insert("queso", 29)

	// See what's left.
	ExpectEq(nil, t.c.LookUp("taco"))
	ExpectEq(17, t.c.LookUp("burrito"))
	ExpectEq(23, t.c.LookUp("enchilada"))
	ExpectEq(29, t.c.LookUp("queso"))
}

func (t *LruCacheTest) Overwrite() {
	t.c.Insert("taco", 17)
	t.c.Insert("taco", 19)
	t.c.Insert("taco", 23)

	ExpectEq(23, t.c.LookUp("taco"))

	// The overwritten entries shouldn't count toward capacity.
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 29)
	t.c.Insert("enchilada", 31)

	ExpectEq(23, t.c.LookUp("taco"))
	ExpectEq(29, t.c.LookUp("burrito"))
	ExpectEq(31, t.c.LookUp("enchilada"))
}

func (t *LruCacheTest) SafeForConcurrentAccess() {
	const numWorkers = 10
	runtime.GOMAXPROCS(numWorkers)

	// Start a few workers writing to and reading from the cache.
	wg := sync.WaitGroup{}

	for i := 0; i < numWorkers; i++ {
		wg.Add(1)
		go func() {
			const numIters = 1000
			for i := 0; i < numIters; i++ {
				key := fmt.Sprintf("%d", i)
				t.c.Insert(key, i)
				val := t.c.LookUp(key)
				if val != nil && val.(int) != i {
					panic(fmt.Sprintf("Unexpected value: %v", val))
				}
			}

			wg.Done()
		}()
	}

	wg.Wait()
}

func (t *LruCacheTest) EncodeAndDecode() {
	AssertEq("TODO", "")
}
<commit_msg>Tuned test.<commit_after>// Copyright 2013 Aaron Jacobs. All Rights Reserved.
// Author: aaronjjacobs@gmail.com (Aaron Jacobs)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cache_test

import (
	"fmt"
	"github.com/jacobsa/comeback/cache"
	. "github.com/jacobsa/oglematchers"
	. "github.com/jacobsa/ogletest"
	"runtime"
	"sync"
	"testing"
)

const cacheCapacity = 3

func TestLruCache(t *testing.T) { RunTests(t) }

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

type LruCacheTest struct {
	c cache.Cache
}

func init() { RegisterTestSuite(&LruCacheTest{}) }

func (t *LruCacheTest) SetUp(i *TestInfo) {
	t.c = cache.NewLruCache(cacheCapacity)
}

////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////

func (t *LruCacheTest) Empty() {
	ExpectEq(nil, t.c.LookUp(""))
	ExpectEq(nil, t.c.LookUp("taco"))
}

func (t *LruCacheTest) InsertNilValue() {
	ExpectThat(
		func() { t.c.Insert("taco", nil) },
		Panics(HasSubstr("nil value")),
	)
}

func (t *LruCacheTest) LookUpUnknownKey() {
	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)

	ExpectEq(nil, t.c.LookUp(""))
	ExpectEq(nil, t.c.LookUp("enchilada"))
}

func (t *LruCacheTest) FillUpToCapacity() {
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)
	t.c.Insert("enchilada", []byte{0x23, 0x29})

	ExpectEq(17, t.c.LookUp("burrito"))
	ExpectEq(19, t.c.LookUp("taco"))
	ExpectThat(t.c.LookUp("enchilada"), DeepEquals([]byte{0x23, 0x29}))
}

func (t *LruCacheTest) ExpiresLeastRecentlyUsed() {
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)  // Least recent
	t.c.Insert("enchilada", 23)  // Second most recent
	AssertEq(17, t.c.LookUp("burrito"))  // Most recent

	// Insert another.
	t.c.Insert("queso", 29)

	// See what's left.
	ExpectEq(nil, t.c.LookUp("taco"))
	ExpectEq(17, t.c.LookUp("burrito"))
	ExpectEq(23, t.c.LookUp("enchilada"))
	ExpectEq(29, t.c.LookUp("queso"))
}

func (t *LruCacheTest) Overwrite() {
	t.c.Insert("taco", 17)
	t.c.Insert("taco", 19)
	t.c.Insert("taco", 23)

	ExpectEq(23, t.c.LookUp("taco"))

	// The overwritten entries shouldn't count toward capacity.
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 29)
	t.c.Insert("enchilada", 31)

	ExpectEq(23, t.c.LookUp("taco"))
	ExpectEq(29, t.c.LookUp("burrito"))
	ExpectEq(31, t.c.LookUp("enchilada"))
}

func (t *LruCacheTest) SafeForConcurrentAccess() {
	const numWorkers = 8
	runtime.GOMAXPROCS(4)

	// Start a few workers writing to and reading from the cache.
	wg := sync.WaitGroup{}

	for i := 0; i < numWorkers; i++ {
		wg.Add(1)
		go func() {
			const numIters = 1e4
			for i := 0; i < numIters; i++ {
				key := fmt.Sprintf("%d", i)
				t.c.Insert(key, i)
				val := t.c.LookUp(key)
				if val != nil && val.(int) != i {
					panic(fmt.Sprintf("Unexpected value: %v", val))
				}
			}

			wg.Done()
		}()
	}

	wg.Wait()
}

func (t *LruCacheTest) EncodeAndDecode() {
	AssertEq("TODO", "")
}
<|endoftext|>


<commit_before>// Copyright 2013 Aaron Jacobs. All Rights Reserved.
// Author: aaronjjacobs@gmail.com (Aaron Jacobs)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cache_test

import (
	"github.com/jacobsa/comeback/cache"
	. "github.com/jacobsa/oglematchers"
	. "github.com/jacobsa/ogletest"
	"testing"
)

const cacheCapacity = 3

func TestLruCache(t *testing.T) { RunTests(t) }

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

type LruCacheTest struct {
	c cache.Cache
}

func init() { RegisterTestSuite(&LruCacheTest{}) }

func (t *LruCacheTest) SetUp(i *TestInfo) {
	t.c = cache.NewLruCache(cacheCapacity)
}

////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////

func (t *LruCacheTest) Empty() {
	ExpectEq(nil, t.c.LookUp(""))
	ExpectEq(nil, t.c.LookUp("taco"))
}

func (t *LruCacheTest) InsertNilValue() {
	ExpectThat(
		func() { t.c.Insert("taco", nil) },
		Panics(HasSubstr("nil value")),
	)
}

func (t *LruCacheTest) LookUpUnknownKey() {
	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)

	ExpectEq(nil, t.c.LookUp(""))
	ExpectEq(nil, t.c.LookUp("enchilada"))
}

func (t *LruCacheTest) FillUpToCapacity() {
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)
	t.c.Insert("enchilada", []byte{0x23, 0x29})

	ExpectEq(17, t.c.LookUp("burrito"))
	ExpectEq(19, t.c.LookUp("taco"))
	ExpectThat(t.c.LookUp("enchilada"), DeepEquals([]byte{0x23, 0x29}))
}

func (t *LruCacheTest) ExpiresLeastRecentlyUsed() {
	AssertEq("TODO", "")
}

func (t *LruCacheTest) Overwrite() {
	AssertEq("TODO", "")
}

func (t *LruCacheTest) SafeForConcurrentAccess() {
	AssertEq("TODO", "")
}

func (t *LruCacheTest) EncodeAndDecode() {
	AssertEq("TODO", "")
}
<commit_msg>LruCacheTest.ExpiresLeastRecentlyUsed<commit_after>// Copyright 2013 Aaron Jacobs. All Rights Reserved.
// Author: aaronjjacobs@gmail.com (Aaron Jacobs)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cache_test

import (
	"github.com/jacobsa/comeback/cache"
	. "github.com/jacobsa/oglematchers"
	. "github.com/jacobsa/ogletest"
	"testing"
)

const cacheCapacity = 3

func TestLruCache(t *testing.T) { RunTests(t) }

////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////

type LruCacheTest struct {
	c cache.Cache
}

func init() { RegisterTestSuite(&LruCacheTest{}) }

func (t *LruCacheTest) SetUp(i *TestInfo) {
	t.c = cache.NewLruCache(cacheCapacity)
}

////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////

func (t *LruCacheTest) Empty() {
	ExpectEq(nil, t.c.LookUp(""))
	ExpectEq(nil, t.c.LookUp("taco"))
}

func (t *LruCacheTest) InsertNilValue() {
	ExpectThat(
		func() { t.c.Insert("taco", nil) },
		Panics(HasSubstr("nil value")),
	)
}

func (t *LruCacheTest) LookUpUnknownKey() {
	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)

	ExpectEq(nil, t.c.LookUp(""))
	ExpectEq(nil, t.c.LookUp("enchilada"))
}

func (t *LruCacheTest) FillUpToCapacity() {
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)
	t.c.Insert("enchilada", []byte{0x23, 0x29})

	ExpectEq(17, t.c.LookUp("burrito"))
	ExpectEq(19, t.c.LookUp("taco"))
	ExpectThat(t.c.LookUp("enchilada"), DeepEquals([]byte{0x23, 0x29}))
}

func (t *LruCacheTest) ExpiresLeastRecentlyUsed() {
	AssertEq(3, cacheCapacity)

	t.c.Insert("burrito", 17)
	t.c.Insert("taco", 19)  // Least recent
	t.c.Insert("enchilada", 23)  // Second most recent
	AssertEq(17, t.c.LookUp("burrito"))  // Most recent

	// Insert another.
	t.c.Insert("queso", 29)

	// See what's left.
	ExpectEq(nil, t.c.LookUp("taco"))
	ExpectEq(17, t.c.LookUp("burrito"))
	ExpectEq(23, t.c.LookUp("enchilada"))
	ExpectEq(29, t.c.LookUp("queso"))
}

func (t *LruCacheTest) Overwrite() {
	AssertEq("TODO", "")
}

func (t *LruCacheTest) SafeForConcurrentAccess() {
	AssertEq("TODO", "")
}

func (t *LruCacheTest) EncodeAndDecode() {
	AssertEq("TODO", "")
}
<|endoftext|>


<commit_before>// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package configs_test

import (
	"path/filepath"
	"strings"
	"testing"

	"google.golang.org/protobuf/encoding/prototext"
	"bitbucket.org/creachadair/stringset"
	spb "github.com/google/localtoast/scannerlib/proto/compliance_go_proto"
	apb "github.com/google/localtoast/scannerlib/proto/api_go_proto"
	ipb "github.com/google/localtoast/scannerlib/proto/scan_instructions_go_proto"
	sipb "github.com/google/localtoast/scannerlib/proto/scan_instructions_go_proto"
)

var configFileNames = stringset.New(
	"vm_image_scanning.textproto", "container_image_scanning.textproto", "instance_scanning.textproto")

// Check if the serialized config uses the new reduced per-OS format.
// The test will skip these so that the migration to the reduced format can
// happen in several steps.
// TODO(b/200859368): Remove the usage of this function and update the tests
// once all configs have been migrated.
func isReducedConfigFile(configBytes []byte) bool {
	config := &apb.PerOsBenchmarkConfig{}
	err := prototext.Unmarshal(configBytes, config)
	return err == nil
}

// Validate behavior across all the configs.
func TestRequiredAttributes(t *testing.T) {
	for filePath, configBytes := range scanConfigDefs {
		config := &apb.ScanConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Errorf("error reading %s: %v", filePath, err)
		}
		for _, b := range config.GetBenchmarkConfigs() {
			noteID := b.GetId()
			note := b.GetComplianceNote()
			if note.GetTitle() == "" {
				t.Errorf(`%s GetTitle(): got "", want non-empty string`, noteID)
			}
			if note.GetDescription() == "" {
				t.Errorf(`%s GetDescription(): got "", want non-empty string`, noteID)
			}
			if note.GetRationale() == "" {
				t.Errorf(`%s GetRationale(): got "", want non-empty string`, noteID)
			}
			if note.GetRemediation() == "" {
				t.Errorf(`%s GetRemediation(): got "", want non-empty string`, noteID)
			}
			if len(note.GetVersion()) == 0 {
				t.Errorf(`%s len(GetVersion()): got 0, want at least 1`, noteID)
			}
			for _, version := range note.GetVersion() {
				if version.GetVersion() == "" {
					t.Errorf(`%s version.GetVersion(): got "", want non-empty string`, noteID)
				}
				if version.GetCpeUri() != "fallback" && version.GetCpeUri() != "cpe:/distribution_independent_linux" && len(strings.Split(version.GetCpeUri(), ":")) != 5 {
					t.Errorf("%s version.getCpeUri(): got %s, want a valid CPE", noteID, version.GetCpeUri())
				}
			}
			if note.GetCisBenchmark().GetProfileLevel() < 1 || note.GetCisBenchmark().GetProfileLevel() > 3 {
				t.Errorf("%s GetCisBenchmark().GetProfilelevel(): got %d, want 1 <= level <= 3", noteID, note.GetCisBenchmark().GetProfileLevel())
			}
			if note.GetCisBenchmark().GetSeverity() == spb.Severity_SEVERITY_UNSPECIFIED {
				t.Errorf("%s GetCisBenchmark().GetSeverity(): got %s, want any specified severity", noteID, note.GetCisBenchmark().GetSeverity())
			}
			scanInstructions := &sipb.BenchmarkScanInstructionDef{}
			if err := prototext.Unmarshal(note.GetScanInstructions(), scanInstructions); err != nil {
				t.Errorf("%s could not parse scan instructions: %v", noteID, err)
			}
		}
	}
}

func TestFilesHaveSupportedName(t *testing.T) {
	for filePath := range reducedScanConfigs {
		fileName := filepath.Base(filePath)
		if !configFileNames.Contains(fileName) {
			t.Errorf("checking file names: got %q, want image_scanning.textproto or instance_scanning.textproto", fileName)
		}
	}
}

func TestScanInstructionsHaveDisplayCommandAndNonComplianceReason(t *testing.T) {
	for filePath, configBytes := range scanConfigDefs {
		config := &apb.ScanConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Fatalf("error reading %s: %v", filePath, err)
		}
		for _, b := range config.GetBenchmarkConfigs() {
			noteID := b.GetId()
			instructionDef := &ipb.BenchmarkScanInstructionDef{}
			if err := prototext.Unmarshal(b.GetComplianceNote().GetScanInstructions(), instructionDef); err != nil {
				t.Errorf("%s could not parse scan instructions: %v", noteID, err)
			}
			var scanInstructions []*ipb.BenchmarkScanInstruction
			if instructionDef.GetGeneric() != nil {
				scanInstructions = []*ipb.BenchmarkScanInstruction{instructionDef.GetGeneric()}
			} else if instructionDef.GetScanTypeSpecific() != nil {
				scanInstructions = []*ipb.BenchmarkScanInstruction{
					instructionDef.GetScanTypeSpecific().InstanceScanning,
					instructionDef.GetScanTypeSpecific().ImageScanning,
				}
			} else {
				t.Fatalf("benchmark %s has invalid instruction def %v", noteID, instructionDef)
			}
			for _, i := range scanInstructions {
				for _, a := range i.GetCheckAlternatives() {
					for _, f := range a.GetFileChecks() {
						if len(f.GetFileDisplayCommand()) > 0 && len(f.GetNonComplianceMsg()) == 0 {
							t.Errorf("check for benchmark %s has a file display command set but no non-compliance message: %v", noteID, f)
						}
					}
				}
			}
		}
	}
}

func TestFallbackBenchmarkDefsHaveExpectedIdFormat(t *testing.T) {
	for filePath, configBytes := range scanConfigDefs {
		if filePath != "fallback.textproto" {
			continue
		}
		if isReducedConfigFile(configBytes) {
			continue
		}
		config := &apb.ScanConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Errorf("error reading %s: %v", filePath, err)
		}
		for _, b := range config.GetBenchmarkConfigs() {
			noteID := b.GetId()
			if !strings.HasSuffix(noteID, "-fallback") {
				t.Errorf("Fallback benchmark ID %q should end with -fallback", noteID)
			}
		}
	}
}

func TestFallbackPerOsBenchmarksHaveExpectedIdFormat(t *testing.T) {
	for filePath, configBytes := range reducedScanConfigs {
		if !strings.Contains(filePath, "/fallback/") {
			continue
		}
		config := &apb.PerOsBenchmarkConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Errorf("error reading %s: %v", filePath, err)
		}
		for _, id := range config.BenchmarkId {
			if !strings.HasSuffix(id, "-fallback") {
				t.Errorf("Fallback benchmark ID %q should end with -fallback", id)
			}
		}
	}
}

func TestProfileLevelOverridesUseExistingIDs(t *testing.T) {
	for filePath, configBytes := range reducedScanConfigs {
		config := &apb.PerOsBenchmarkConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Errorf("error reading %s: %v", filePath, err)
		}
		ids := make(map[string]bool)
		for _, id := range config.BenchmarkId {
			ids[id] = true
		}
		for _, o := range config.ProfileLevelOverride {
			for _, id := range o.BenchmarkId {
				if _, ok := ids[id]; !ok {
					t.Errorf("%s: overridden benchmark ID %q isn't used by the config", filePath, id)
				}
			}
		}
	}
}
<commit_msg>Read the scan config files from the FS in configs_test.<commit_after>// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package configs_test

import (
	"io/ioutil"
	"log"
	"os"
	"path"
	"path/filepath"
	"strings"
	"testing"

	"google.golang.org/protobuf/encoding/prototext"
	"bitbucket.org/creachadair/stringset"
	spb "github.com/google/localtoast/scannerlib/proto/compliance_go_proto"
	apb "github.com/google/localtoast/scannerlib/proto/api_go_proto"
	ipb "github.com/google/localtoast/scannerlib/proto/scan_instructions_go_proto"
	sipb "github.com/google/localtoast/scannerlib/proto/scan_instructions_go_proto"
)

const (
	configDefPath     = "defs/"
	reducedConfigPath = "reduced/"
)

var configFileNames = stringset.New(
	"vm_image_scanning.textproto", "container_image_scanning.textproto", "instance_scanning.textproto")

var scanConfigDefs, reducedScanConfigs = readConfigFiles()

func readConfigFiles() (map[string][]byte, map[string][]byte) {
	return readFilesInDir(configDefPath), readFilesInDir(reducedConfigPath)
}

func readFilesInDir(dirPath string) map[string][]byte {
	result := make(map[string][]byte)
	err := filepath.Walk(dirPath,
		func(filePath string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if path.Ext(filePath) != ".textproto" {
				return nil
			}
			content, err := ioutil.ReadFile(filePath)
			if err != nil {
				return err
			}
			result[filePath] = content
			return nil
		})
	if err != nil {
		log.Fatalf("Error reading scan config defs %v\n", err)
	}
	return result
}

// Check if the serialized config uses the new reduced per-OS format.
// The test will skip these so that the migration to the reduced format can
// happen in several steps.
// TODO(b/200859368): Remove the usage of this function and update the tests
// once all configs have been migrated.
func isReducedConfigFile(configBytes []byte) bool {
	config := &apb.PerOsBenchmarkConfig{}
	err := prototext.Unmarshal(configBytes, config)
	return err == nil
}

// Validate behavior across all the configs.
func TestRequiredAttributes(t *testing.T) {
	for filePath, configBytes := range scanConfigDefs {
		config := &apb.ScanConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Errorf("error reading %s: %v", filePath, err)
		}
		for _, b := range config.GetBenchmarkConfigs() {
			noteID := b.GetId()
			note := b.GetComplianceNote()
			if note.GetTitle() == "" {
				t.Errorf(`%s GetTitle(): got "", want non-empty string`, noteID)
			}
			if note.GetDescription() == "" {
				t.Errorf(`%s GetDescription(): got "", want non-empty string`, noteID)
			}
			if note.GetRationale() == "" {
				t.Errorf(`%s GetRationale(): got "", want non-empty string`, noteID)
			}
			if note.GetRemediation() == "" {
				t.Errorf(`%s GetRemediation(): got "", want non-empty string`, noteID)
			}
			if len(note.GetVersion()) == 0 {
				t.Errorf(`%s len(GetVersion()): got 0, want at least 1`, noteID)
			}
			for _, version := range note.GetVersion() {
				if version.GetVersion() == "" {
					t.Errorf(`%s version.GetVersion(): got "", want non-empty string`, noteID)
				}
				if version.GetCpeUri() != "fallback" && version.GetCpeUri() != "cpe:/distribution_independent_linux" && len(strings.Split(version.GetCpeUri(), ":")) != 5 {
					t.Errorf("%s version.getCpeUri(): got %s, want a valid CPE", noteID, version.GetCpeUri())
				}
			}
			if note.GetCisBenchmark().GetProfileLevel() < 1 || note.GetCisBenchmark().GetProfileLevel() > 3 {
				t.Errorf("%s GetCisBenchmark().GetProfilelevel(): got %d, want 1 <= level <= 3", noteID, note.GetCisBenchmark().GetProfileLevel())
			}
			if note.GetCisBenchmark().GetSeverity() == spb.Severity_SEVERITY_UNSPECIFIED {
				t.Errorf("%s GetCisBenchmark().GetSeverity(): got %s, want any specified severity", noteID, note.GetCisBenchmark().GetSeverity())
			}
			scanInstructions := &sipb.BenchmarkScanInstructionDef{}
			if err := prototext.Unmarshal(note.GetScanInstructions(), scanInstructions); err != nil {
				t.Errorf("%s could not parse scan instructions: %v", noteID, err)
			}
		}
	}
}

func TestFilesHaveSupportedName(t *testing.T) {
	for filePath := range reducedScanConfigs {
		fileName := filepath.Base(filePath)
		if !configFileNames.Contains(fileName) {
			t.Errorf("checking file names: got %q, want image_scanning.textproto or instance_scanning.textproto", fileName)
		}
	}
}

func TestScanInstructionsHaveDisplayCommandAndNonComplianceReason(t *testing.T) {
	for filePath, configBytes := range scanConfigDefs {
		config := &apb.ScanConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Fatalf("error reading %s: %v", filePath, err)
		}
		for _, b := range config.GetBenchmarkConfigs() {
			noteID := b.GetId()
			instructionDef := &ipb.BenchmarkScanInstructionDef{}
			if err := prototext.Unmarshal(b.GetComplianceNote().GetScanInstructions(), instructionDef); err != nil {
				t.Errorf("%s could not parse scan instructions: %v", noteID, err)
			}
			var scanInstructions []*ipb.BenchmarkScanInstruction
			if instructionDef.GetGeneric() != nil {
				scanInstructions = []*ipb.BenchmarkScanInstruction{instructionDef.GetGeneric()}
			} else if instructionDef.GetScanTypeSpecific() != nil {
				scanInstructions = []*ipb.BenchmarkScanInstruction{
					instructionDef.GetScanTypeSpecific().InstanceScanning,
					instructionDef.GetScanTypeSpecific().ImageScanning,
				}
			} else {
				t.Fatalf("benchmark %s has invalid instruction def %v", noteID, instructionDef)
			}
			for _, i := range scanInstructions {
				for _, a := range i.GetCheckAlternatives() {
					for _, f := range a.GetFileChecks() {
						if len(f.GetFileDisplayCommand()) > 0 && len(f.GetNonComplianceMsg()) == 0 {
							t.Errorf("check for benchmark %s has a file display command set but no non-compliance message: %v", noteID, f)
						}
					}
				}
			}
		}
	}
}

func TestFallbackBenchmarkDefsHaveExpectedIdFormat(t *testing.T) {
	for filePath, configBytes := range scanConfigDefs {
		if filePath != "fallback.textproto" {
			continue
		}
		if isReducedConfigFile(configBytes) {
			continue
		}
		config := &apb.ScanConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Errorf("error reading %s: %v", filePath, err)
		}
		for _, b := range config.GetBenchmarkConfigs() {
			noteID := b.GetId()
			if !strings.HasSuffix(noteID, "-fallback") {
				t.Errorf("Fallback benchmark ID %q should end with -fallback", noteID)
			}
		}
	}
}

func TestFallbackPerOsBenchmarksHaveExpectedIdFormat(t *testing.T) {
	for filePath, configBytes := range reducedScanConfigs {
		if !strings.Contains(filePath, "/fallback/") {
			continue
		}
		config := &apb.PerOsBenchmarkConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Errorf("error reading %s: %v", filePath, err)
		}
		for _, id := range config.BenchmarkId {
			if !strings.HasSuffix(id, "-fallback") {
				t.Errorf("Fallback benchmark ID %q should end with -fallback", id)
			}
		}
	}
}

func TestProfileLevelOverridesUseExistingIDs(t *testing.T) {
	for filePath, configBytes := range reducedScanConfigs {
		config := &apb.PerOsBenchmarkConfig{}
		if err := prototext.Unmarshal(configBytes, config); err != nil {
			t.Errorf("error reading %s: %v", filePath, err)
		}
		ids := make(map[string]bool)
		for _, id := range config.BenchmarkId {
			ids[id] = true
		}
		for _, o := range config.ProfileLevelOverride {
			for _, id := range o.BenchmarkId {
				if _, ok := ids[id]; !ok {
					t.Errorf("%s: overridden benchmark ID %q isn't used by the config", filePath, id)
				}
			}
		}
	}
}
<|endoftext|>


<commit_before>package gmqconf

import (
	"encoding/json"
	"errors"
	"io/ioutil"
)

const (
	USE_MEMORY     = 1
	USE_DATABASE   = 2
	USE_FILESYSTEM = 3
)

type NetConf struct {
	Port  string `json:"port"`
	Inet  string `json:"interface"`
	Proto string `json:"protocol"`
}

type QConf struct {
	QueueType   int `json:"queue_type"`
	MaxQueueN   int `json:"max_queue_number"`
	MaxMessageL int `json:"max_message_length"`
	MaxQueueC   int `json:"max_queue_capacity"`
}

type DbConf struct {
	DbVendor string `json:"vendor"`
	Dsn      string `json:"dsn"`
}

type FsConf struct {
	Path string `json:"path"`
}

type LogConf struct {
	Path string `json:"path"`
	Name string `json:"name"`
}

type ClusterConf struct {
	Cidr        string `json:"cidr"`
	Port        string `json:cluster_port`
	TimeoutMsec int64  `json:"timeout_msec"`
}

type Params struct {
	Network NetConf     `json:"network"`
	Cluster ClusterConf `json:"cluster"`
	Queue   QConf       `json:"queue"`
	Db      DbConf      `json:"database"`
	Fs      FsConf      `json:"filesystem"`
	Log     LogConf     `json:"log"`
}

func ParseConfiguration(file string) (*Params, error) {
	params := new(Params)
	conf, err := ioutil.ReadFile(file)
	if err != nil {
		return nil, errors.New("Configuration file not available")
	}
	if err := json.Unmarshal(conf, params); err != nil {
		return nil, errors.New("Unable to parse configuration file, check JSON is well formed")
	}
	return params, err
}
<commit_msg>Corrected configuration for CLUSTER<commit_after>package gmqconf

import (
	"encoding/json"
	"errors"
	"io/ioutil"
)

const (
	USE_MEMORY     = 1
	USE_DATABASE   = 2
	USE_FILESYSTEM = 3
)

type NetConf struct {
	Port  string `json:"port"`
	Inet  string `json:"interface"`
	Proto string `json:"protocol"`
}

type QConf struct {
	QueueType   int `json:"queue_type"`
	MaxQueueN   int `json:"max_queue_number"`
	MaxMessageL int `json:"max_message_length"`
	MaxQueueC   int `json:"max_queue_capacity"`
}

type DbConf struct {
	DbVendor string `json:"vendor"`
	Dsn      string `json:"dsn"`
}

type FsConf struct {
	Path string `json:"path"`
}

type LogConf struct {
	Path string `json:"path"`
	Name string `json:"name"`
}

type ClusterConf struct {
	Network     string `json:"network"`
	Port        string `json:"cluster_port"`
	TimeoutMsec int64  `json:"timeout_msec"`
}

type Params struct {
	Network NetConf     `json:"network"`
	Cluster ClusterConf `json:"cluster"`
	Queue   QConf       `json:"queue"`
	Db      DbConf      `json:"database"`
	Fs      FsConf      `json:"filesystem"`
	Log     LogConf     `json:"log"`
}

func ParseConfiguration(file string) (*Params, error) {
	params := new(Params)
	conf, err := ioutil.ReadFile(file)
	if err != nil {
		return nil, errors.New("Configuration file not available")
	}
	if err := json.Unmarshal(conf, params); err != nil {
		return nil, errors.New("Unable to parse configuration file, check JSON is well formed")
	}
	return params, err
}
<|endoftext|>


<commit_before>package changeorg

import (
	"encoding/json"
	"errors"
	"io/ioutil"
	"net/http"
	"net/url"
)

const (
	Host = "https://api.change.org/v1/"
)

type ChangeOrg struct {
	Key  string
	Host string
}

func NewChangeOrgClient(key string) *ChangeOrg {
	return &ChangeOrg{Key: key, Host: Host}
}

func (c *ChangeOrg) Petition_Id(args PetitionIdArgs) (*int, error) {
	v := url.Values{}
	v.Set("api_key", c.Key)
	if len(args.PetitionURL) > 0 {
		v.Set("petition_url", args.PetitionURL)
	}
	url := c.Host + "petitions/get_id?" + v.Encode()
	res, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	var bodyRes Response
	if err := unmarshal(res, &bodyRes); err != nil {
		return nil, err
	}
	return &bodyRes.PetitionID, err
}

func unmarshal(res *http.Response, bodyRes *Response) error {
	b, err := ioutil.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return err
	}
	if err := json.Unmarshal(b, &bodyRes); err != nil {
		return err
	}
	if len(bodyRes.Messages) > 0 {
		msg := ""
		for i := range bodyRes.Messages {
			msg += bodyRes.Messages[i]
		}
		return errors.New(msg)
	}
	return nil
}
<commit_msg>change function signature<commit_after>package changeorg

import (
	"encoding/json"
	"errors"
	"io/ioutil"
	"net/http"
	"net/url"
)

const (
	Host = "https://api.change.org/v1/"
)

type ChangeOrg struct {
	Key  string
	Host string
}

func NewChangeOrgClient(key string) *ChangeOrg {
	return &ChangeOrg{Key: key, Host: Host}
}

func (c *ChangeOrg) GetPetitionId(args PetitionIdArgs) (*int, error) {
	v := url.Values{}
	v.Set("api_key", c.Key)
	if len(args.PetitionURL) > 0 {
		v.Set("petition_url", args.PetitionURL)
	}
	url := c.Host + "petitions/get_id?" + v.Encode()
	res, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	var bodyRes Response
	if err := unmarshal(res, &bodyRes); err != nil {
		return nil, err
	}
	return &bodyRes.PetitionID, err
}

func unmarshal(res *http.Response, bodyRes *Response) error {
	b, err := ioutil.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return err
	}
	if err := json.Unmarshal(b, &bodyRes); err != nil {
		return err
	}
	if len(bodyRes.Messages) > 0 {
		msg := ""
		for i := range bodyRes.Messages {
			msg += bodyRes.Messages[i]
		}
		return errors.New(msg)
	}
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2016, RadiantBlue Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package systest

import (
	"bytes"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"testing"
	"time"

	"encoding/json"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
	"github.com/venicegeo/pz-gocommon/gocommon"
	pzsyslog "github.com/venicegeo/pz-gocommon/syslog"
)

func sleep(n int) {
	time.Sleep(time.Second * time.Duration(n))
}

type LoggerTester struct {
	suite.Suite

	logWriter   pzsyslog.Writer
	httpWriter  *pzsyslog.HttpWriter // just a typed copy of logWriter
	auditWriter pzsyslog.Writer

	logger    *pzsyslog.Logger
	apiKey    string
	apiHost   string
	loggerUrl string

	mssgHostName    string
	mssgApplication string
	mssgProcess     string
	mmsgSeverity    pzsyslog.Severity
}

func (suite *LoggerTester) setupFixture() {
	t := suite.T()
	assert := assert.New(t)

	var err error

	suite.apiHost, err = piazza.GetApiServer()
	if err != nil {
		assert.FailNow(err.Error())
	}

	// note that we are NOT using the gateway
	suite.loggerUrl, err = piazza.GetPiazzaServiceUrl(piazza.PzLogger)
	assert.NoError(err)

	suite.apiKey, err = piazza.GetApiKey(suite.apiHost)
	assert.NoError(err)

	suite.httpWriter, err = pzsyslog.NewHttpWriter(suite.loggerUrl, suite.apiKey)
	suite.logWriter = suite.httpWriter
	assert.NoError(err)

	suite.auditWriter, err = pzsyslog.NewHttpWriter(suite.loggerUrl, suite.apiKey)

	suite.mssgHostName, err = piazza.GetExternalIP()
	assert.NoError(err)
	suite.mssgApplication = "pz-logger/systest"
	suite.mssgProcess = strconv.Itoa(os.Getpid())
	suite.mmsgSeverity = pzsyslog.Informational

	suite.logger = pzsyslog.NewLogger(suite.logWriter, suite.auditWriter, suite.mssgApplication)
}

func (suite *LoggerTester) teardownFixture() {
	t := suite.T()
	assert := assert.New(t)

	err := suite.logWriter.Close()
	assert.NoError(err)
}

func TestRunSuite(t *testing.T) {
	s := &LoggerTester{}
	suite.Run(t, s)
}

func (suite *LoggerTester) verifyMessage(expected string) {
	format := &piazza.JsonPagination{
		PerPage: 500, // has to be this high, in case logger is under high load
		Page:    0,
		SortBy:  "timeStamp",
		Order:   piazza.SortOrderDescending,
	}
	params := &piazza.HttpQueryParams{}
	suite.verifyMessageF(format, params, expected)
}

func (suite *LoggerTester) verifyMessageF(
	format *piazza.JsonPagination,
	params *piazza.HttpQueryParams,
	expected string,
) {
	t := suite.T()
	assert := assert.New(t)

	ms, _, err := suite.httpWriter.GetMessages(format, params)
	assert.NoError(err)
	assert.Len(ms, format.PerPage)

	ok := false
	for _, m := range ms {
		if m.Message == expected {
			ok = true
			break
		}
	}
	assert.True(ok)
}

func (suite *LoggerTester) getVersion() (*piazza.Version, error) {
	h := &piazza.Http{BaseUrl: suite.loggerUrl}
	jresp := h.PzGet("/version")
	if jresp.IsError() {
		return nil, jresp.ToError()
	}

	var version piazza.Version
	err := jresp.ExtractData(&version)
	if err != nil {
		return nil, err
	}

	return &version, nil
}

func (suite *LoggerTester) getStats(output interface{}) error {
	h := &piazza.Http{BaseUrl: suite.loggerUrl}

	jresp := h.PzGet("/admin/stats")
	if jresp.IsError() {
		return jresp.ToError()
	}

	return jresp.ExtractData(output)
}

func (suite *LoggerTester) makeMessage(text string) *pzsyslog.Message {
	t := suite.T()
	assert := assert.New(t)

	var err error

	m := pzsyslog.NewMessage()
	m.Message = text
	m.HostName, err = piazza.GetExternalIP()
	assert.NoError(err)
	m.Application = "pzlogger-systest"
	m.Process = strconv.Itoa(os.Getpid())
	m.Severity = pzsyslog.Informational

	return m
}

func (suite *LoggerTester) Test00Version() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	version, err := suite.getVersion()
	assert.NoError(err)
	assert.EqualValues("1.0.0", version.Version)
}

func (suite *LoggerTester) Test01RawGet() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()
	resp, err := http.Get(suite.loggerUrl + "/syslog")
	assert.NoError(err)
	if resp.ContentLength <= 0 {
		log.Printf("content-length is <= 0")
		panic(99)
	}
	assert.True(resp.ContentLength > 0)

	raw := make([]byte, resp.ContentLength)
	_, err = io.ReadFull(resp.Body, raw)
	defer func() {
		err = resp.Body.Close()
		assert.NoError(err)
	}()
	if err != nil && err != io.EOF {
		assert.NoError(err)
	}

	assert.Equal(200, resp.StatusCode)
}

func (suite *LoggerTester) Test02RawPost() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	mssg := suite.makeMessage("Test02")

	jsn, err := json.Marshal(mssg)
	reader := bytes.NewReader(jsn)

	resp, err := http.Post(suite.loggerUrl+"/syslog",
		piazza.ContentTypeJSON, reader)
	assert.NoError(err)

	raw := make([]byte, resp.ContentLength)
	_, err = io.ReadFull(resp.Body, raw)
	defer func() {
		err = resp.Body.Close()
		assert.NoError(err)
	}()
	if err != nil && err != io.EOF {
		assert.NoError(err)
	}

	assert.Equal(200, resp.StatusCode)
}

func (suite *LoggerTester) Test03Get() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	format := &piazza.JsonPagination{
		PerPage: 12,
		Page:    0,
		Order:   piazza.SortOrderDescending,
		SortBy:  "hostName",
	}
	ms, _, err := suite.httpWriter.GetMessages(format, nil)
	assert.NoError(err)
	assert.Len(ms, format.PerPage)

	assert.False(time.Time(ms[0].TimeStamp).IsZero())
}

func (suite *LoggerTester) Test04Post() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	var err error

	nowz := piazza.NewTimeStamp()
	keyz := "KEYZ KEYZ KEYZ " + nowz.String()

	mssgz := suite.makeMessage(keyz)
	mssgz.TimeStamp = nowz

	err = suite.httpWriter.Write(mssgz)
	assert.NoError(err)

	// allow ES to catch up
	sleep(2)

	suite.verifyMessage(keyz)
}

func (suite *LoggerTester) Test05Logger() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	uniq := "Test05/" + time.Now().String()

	err := suite.logger.Info(uniq)
	assert.NoError(err)

	sleep(2)

	suite.verifyMessage(uniq)
}

func (suite *LoggerTester) Test06Admin() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	output := map[string]interface{}{}
	err := suite.getStats(&output)
	assert.NoError(err)
	assert.NotZero(output["numMessages"])
}

func (suite *LoggerTester) Test07Pagination() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	format := &piazza.JsonPagination{
		PerPage: 10,
		Page:    0,
		SortBy:  "timeStamp",
		Order:   piazza.SortOrderAscending,
	}
	params := &piazza.HttpQueryParams{}

	// check per-page
	{
		format.PerPage = 17
		ms, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)
		assert.Len(ms, 17)
	}

	// check sort order
	{
		format.PerPage = 10
		format.Order = piazza.SortOrderAscending
		ms, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)
		last := len(ms) - 1
		assert.True(last <= 9)

		// we can't check "strictly before", because two timeStamps might be the same
		t0 := time.Time(ms[0].TimeStamp)
		tlast := time.Time(ms[last].TimeStamp)
		isBefore := t0.Before(tlast)
		isEqual := t0.Equal(tlast)
		assert.True(isBefore || isEqual)

		format.Order = piazza.SortOrderDescending
		ms, _, err = suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)
		last = len(ms) - 1
		assert.True(last <= 9)

		t0 = time.Time(ms[0].TimeStamp)
		tlast = time.Time(ms[last].TimeStamp)
		isAfter := t0.After(tlast)
		isEqual = t0.Equal(tlast)
		assert.True(isAfter || isEqual)
	}

	// check sort-by
	{
		format.Order = piazza.SortOrderAscending
		format.SortBy = "severity"
		format.PerPage = 100
		format.Page = 0
		ms, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)

		last := len(ms) - 1
		for i := 0; i < last; i++ {
			a, b := ms[i].Severity, ms[i+1].Severity
			isSameOrBefore := (a <= b)
			assert.True(isSameOrBefore)
		}
	}
}

func (suite *LoggerTester) Test08Params() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	uniq := "Test08/" + strconv.Itoa(time.Now().Nanosecond())

	delta := time.Duration(10 * time.Second)
	tstart := time.Now().Add(-delta).UTC()

	err := suite.logger.Information(uniq)
	assert.NoError(err)

	tend := time.Now().Add(delta).UTC()

	sleep(1)

	format := &piazza.JsonPagination{
		PerPage: 256,
		Page:    0,
		Order:   piazza.SortOrderDescending,
		SortBy:  "timeStamp",
	}

	// test date range params
	{

		params := &piazza.HttpQueryParams{}
		params.AddTime("after", tstart)
		params.AddTime("before", tend)

		msgs, cnt, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)

		assert.True(cnt >= 1)
		assert.True(len(msgs) >= 1)
	}

	// test service param
	{
		params := &piazza.HttpQueryParams{}
		params.AddString("service", suite.mssgApplication)

		msgs, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)

		assert.Len(msgs, 1)
	}

	// test contains param
	{
		params := &piazza.HttpQueryParams{}
		params.AddString("contains", suite.mssgHostName)

		msgs, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)

		assert.True(len(msgs) >= 1)
	}
}

func (suite *LoggerTester) Test09Query() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	jsn := `
{
    "query": {
        "match_all": {}
    },
	"size": 5,
	"from": 0,
	"sort": {
		"timeStamp": "asc"
	}
}`

	h := &piazza.Http{BaseUrl: suite.loggerUrl}
	resp := h.PzPost("/query", jsn)

	assert.False(resp.IsError())
	assert.Nil(resp.ToError())

	data := []pzsyslog.Message{}
	err := resp.ExtractData(&data)
	assert.NoError(err)

	assert.Len(data, 5)
}
<commit_msg>compile fix<commit_after>// Copyright 2016, RadiantBlue Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package systest

import (
	"bytes"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"testing"
	"time"

	"encoding/json"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
	"github.com/venicegeo/pz-gocommon/gocommon"
	pzsyslog "github.com/venicegeo/pz-gocommon/syslog"
)

func sleep(n int) {
	time.Sleep(time.Second * time.Duration(n))
}

type LoggerTester struct {
	suite.Suite

	logWriter   pzsyslog.Writer
	httpWriter  *pzsyslog.HttpWriter // just a typed copy of logWriter
	auditWriter pzsyslog.Writer

	logger    *pzsyslog.Logger
	apiKey    string
	apiHost   string
	loggerUrl string

	mssgHostName    string
	mssgApplication string
	mssgProcess     string
	mmsgSeverity    pzsyslog.Severity
}

func (suite *LoggerTester) setupFixture() {
	t := suite.T()
	assert := assert.New(t)

	var err error

	suite.apiHost, err = piazza.GetApiServer()
	if err != nil {
		assert.FailNow(err.Error())
	}

	// note that we are NOT using the gateway
	suite.loggerUrl, err = piazza.GetPiazzaServiceUrl(piazza.PzLogger)
	assert.NoError(err)

	suite.apiKey, err = piazza.GetApiKey(suite.apiHost)
	assert.NoError(err)

	suite.httpWriter, err = pzsyslog.NewHttpWriter(suite.loggerUrl, suite.apiKey)
	suite.logWriter = suite.httpWriter
	assert.NoError(err)

	suite.auditWriter, err = pzsyslog.NewHttpWriter(suite.loggerUrl, suite.apiKey)

	suite.mssgHostName, err = piazza.GetExternalIP()
	assert.NoError(err)
	suite.mssgApplication = "pz-logger/systest"
	suite.mssgProcess = strconv.Itoa(os.Getpid())
	suite.mmsgSeverity = pzsyslog.Informational

	suite.logger = pzsyslog.NewLogger(suite.logWriter, suite.auditWriter, suite.mssgApplication)
}

func (suite *LoggerTester) teardownFixture() {
	t := suite.T()
	assert := assert.New(t)

	err := suite.logWriter.Close()
	assert.NoError(err)
}

func TestRunSuite(t *testing.T) {
	s := &LoggerTester{}
	suite.Run(t, s)
}

func (suite *LoggerTester) verifyMessage(expected string) {
	format := &piazza.JsonPagination{
		PerPage: 500, // has to be this high, in case logger is under high load
		Page:    0,
		SortBy:  "timeStamp",
		Order:   piazza.SortOrderDescending,
	}
	params := &piazza.HttpQueryParams{}
	suite.verifyMessageF(format, params, expected)
}

func (suite *LoggerTester) verifyMessageF(
	format *piazza.JsonPagination,
	params *piazza.HttpQueryParams,
	expected string,
) {
	t := suite.T()
	assert := assert.New(t)

	ms, _, err := suite.httpWriter.GetMessages(format, params)
	assert.NoError(err)
	assert.Len(ms, format.PerPage)

	ok := false
	for _, m := range ms {
		if m.Message == expected {
			ok = true
			break
		}
	}
	assert.True(ok)
}

func (suite *LoggerTester) getVersion() (*piazza.Version, error) {
	h := &piazza.Http{BaseUrl: suite.loggerUrl}
	jresp := h.PzGet("/version")
	if jresp.IsError() {
		return nil, jresp.ToError()
	}

	var version piazza.Version
	err := jresp.ExtractData(&version)
	if err != nil {
		return nil, err
	}

	return &version, nil
}

func (suite *LoggerTester) getStats(output interface{}) error {
	h := &piazza.Http{BaseUrl: suite.loggerUrl}

	jresp := h.PzGet("/admin/stats")
	if jresp.IsError() {
		return jresp.ToError()
	}

	return jresp.ExtractData(output)
}

func (suite *LoggerTester) makeMessage(text string) *pzsyslog.Message {
	t := suite.T()
	assert := assert.New(t)

	var err error

	m := pzsyslog.NewMessage()
	m.Message = text
	m.HostName, err = piazza.GetExternalIP()
	assert.NoError(err)
	m.Application = "pzlogger-systest"
	m.Process = strconv.Itoa(os.Getpid())
	m.Severity = pzsyslog.Informational

	return m
}

func (suite *LoggerTester) Test00Version() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	version, err := suite.getVersion()
	assert.NoError(err)
	assert.EqualValues("1.0.0", version.Version)
}

func (suite *LoggerTester) Test01RawGet() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()
	resp, err := http.Get(suite.loggerUrl + "/syslog")
	assert.NoError(err)
	if resp.ContentLength <= 0 {
		log.Printf("content-length is <= 0")
		panic(99)
	}
	assert.True(resp.ContentLength > 0)

	raw := make([]byte, resp.ContentLength)
	_, err = io.ReadFull(resp.Body, raw)
	defer func() {
		err = resp.Body.Close()
		assert.NoError(err)
	}()
	if err != nil && err != io.EOF {
		assert.NoError(err)
	}

	assert.Equal(200, resp.StatusCode)
}

func (suite *LoggerTester) Test02RawPost() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	mssg := suite.makeMessage("Test02")

	jsn, err := json.Marshal(mssg)
	reader := bytes.NewReader(jsn)

	resp, err := http.Post(suite.loggerUrl+"/syslog",
		piazza.ContentTypeJSON, reader)
	assert.NoError(err)

	raw := make([]byte, resp.ContentLength)
	_, err = io.ReadFull(resp.Body, raw)
	defer func() {
		err = resp.Body.Close()
		assert.NoError(err)
	}()
	if err != nil && err != io.EOF {
		assert.NoError(err)
	}

	assert.Equal(200, resp.StatusCode)
}

func (suite *LoggerTester) Test03Get() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	format := &piazza.JsonPagination{
		PerPage: 12,
		Page:    0,
		Order:   piazza.SortOrderDescending,
		SortBy:  "hostName",
	}
	ms, _, err := suite.httpWriter.GetMessages(format, nil)
	assert.NoError(err)
	assert.Len(ms, format.PerPage)

	assert.False(time.Time(ms[0].TimeStamp).IsZero())
}

func (suite *LoggerTester) Test04Post() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	var err error

	nowz := piazza.NewTimeStamp()
	keyz := "KEYZ KEYZ KEYZ " + nowz.String()

	mssgz := suite.makeMessage(keyz)
	mssgz.TimeStamp = nowz

	err = suite.httpWriter.Write(mssgz, false)
	assert.NoError(err)

	// allow ES to catch up
	sleep(2)

	suite.verifyMessage(keyz)
}

func (suite *LoggerTester) Test05Logger() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	uniq := "Test05/" + time.Now().String()

	err := suite.logger.Info(uniq)
	assert.NoError(err)

	sleep(2)

	suite.verifyMessage(uniq)
}

func (suite *LoggerTester) Test06Admin() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	output := map[string]interface{}{}
	err := suite.getStats(&output)
	assert.NoError(err)
	assert.NotZero(output["numMessages"])
}

func (suite *LoggerTester) Test07Pagination() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	format := &piazza.JsonPagination{
		PerPage: 10,
		Page:    0,
		SortBy:  "timeStamp",
		Order:   piazza.SortOrderAscending,
	}
	params := &piazza.HttpQueryParams{}

	// check per-page
	{
		format.PerPage = 17
		ms, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)
		assert.Len(ms, 17)
	}

	// check sort order
	{
		format.PerPage = 10
		format.Order = piazza.SortOrderAscending
		ms, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)
		last := len(ms) - 1
		assert.True(last <= 9)

		// we can't check "strictly before", because two timeStamps might be the same
		t0 := time.Time(ms[0].TimeStamp)
		tlast := time.Time(ms[last].TimeStamp)
		isBefore := t0.Before(tlast)
		isEqual := t0.Equal(tlast)
		assert.True(isBefore || isEqual)

		format.Order = piazza.SortOrderDescending
		ms, _, err = suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)
		last = len(ms) - 1
		assert.True(last <= 9)

		t0 = time.Time(ms[0].TimeStamp)
		tlast = time.Time(ms[last].TimeStamp)
		isAfter := t0.After(tlast)
		isEqual = t0.Equal(tlast)
		assert.True(isAfter || isEqual)
	}

	// check sort-by
	{
		format.Order = piazza.SortOrderAscending
		format.SortBy = "severity"
		format.PerPage = 100
		format.Page = 0
		ms, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)

		last := len(ms) - 1
		for i := 0; i < last; i++ {
			a, b := ms[i].Severity, ms[i+1].Severity
			isSameOrBefore := (a <= b)
			assert.True(isSameOrBefore)
		}
	}
}

func (suite *LoggerTester) Test08Params() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	uniq := "Test08/" + strconv.Itoa(time.Now().Nanosecond())

	delta := time.Duration(10 * time.Second)
	tstart := time.Now().Add(-delta).UTC()

	err := suite.logger.Information(uniq)
	assert.NoError(err)

	tend := time.Now().Add(delta).UTC()

	sleep(1)

	format := &piazza.JsonPagination{
		PerPage: 256,
		Page:    0,
		Order:   piazza.SortOrderDescending,
		SortBy:  "timeStamp",
	}

	// test date range params
	{

		params := &piazza.HttpQueryParams{}
		params.AddTime("after", tstart)
		params.AddTime("before", tend)

		msgs, cnt, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)

		assert.True(cnt >= 1)
		assert.True(len(msgs) >= 1)
	}

	// test service param
	{
		params := &piazza.HttpQueryParams{}
		params.AddString("service", suite.mssgApplication)

		msgs, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)

		assert.Len(msgs, 1)
	}

	// test contains param
	{
		params := &piazza.HttpQueryParams{}
		params.AddString("contains", suite.mssgHostName)

		msgs, _, err := suite.httpWriter.GetMessages(format, params)
		assert.NoError(err)

		assert.True(len(msgs) >= 1)
	}
}

func (suite *LoggerTester) Test09Query() {
	t := suite.T()
	assert := assert.New(t)

	suite.setupFixture()
	defer suite.teardownFixture()

	jsn := `
{
    "query": {
        "match_all": {}
    },
	"size": 5,
	"from": 0,
	"sort": {
		"timeStamp": "asc"
	}
}`

	h := &piazza.Http{BaseUrl: suite.loggerUrl}
	resp := h.PzPost("/query", jsn)

	assert.False(resp.IsError())
	assert.Nil(resp.ToError())

	data := []pzsyslog.Message{}
	err := resp.ExtractData(&data)
	assert.NoError(err)

	assert.Len(data, 5)
}
<|endoftext|>