Bladeren bron

Add first integration test

Frédéric Guillot 8 jaren geleden
bovenliggende
commit
142e8b3e0c

+ 6 - 1
.travis.yml

@@ -1,5 +1,9 @@
 notifications:
   email: false
+services:
+  - postgresql
+addons:
+  postgresql: "9.4"
 language: go
 go:
   - 1.9
@@ -7,4 +11,5 @@ before_install:
   - npm install -g jshint
 script:
   - jshint server/static/js/app.js
-  - go test -cover -race ./...
+  - make test
+  - make integration-test

+ 7 - 1
Gopkg.lock

@@ -37,6 +37,12 @@
   packages = [".","hstore","oid"]
   revision = "8c6ee72f3e6bcb1542298dd5f76cb74af9742cec"
 
+[[projects]]
+  branch = "master"
+  name = "github.com/miniflux/miniflux-go"
+  packages = ["."]
+  revision = "a2caa9187ebe4378f36c8e680825586a890154c9"
+
 [[projects]]
   name = "github.com/tdewolff/minify"
   packages = [".","css","js"]
@@ -94,6 +100,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "62e971001374c00b358ba709b9694393eec4a1a84d94a946b21251fa81d98af7"
+  inputs-digest = "ade513f4a86a1f49ce3508aa55f40cf2b1f172d6f3679649e48de67a82715431"
   solver-name = "gps-cdcl"
   solver-version = 1

+ 18 - 1
Makefile

@@ -1,8 +1,9 @@
 APP = miniflux
 VERSION = $(shell git rev-parse --short HEAD)
 BUILD_DATE = `date +%FT%T%z`
+DB_URL = postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
 
-.PHONY: build-linux build-darwin build run clean test
+.PHONY: build-linux build-darwin build run clean test integration-test clean-integration-test
 
 build-linux:
 	@ go generate
@@ -23,3 +24,19 @@ clean:
 
 test:
 	go test -cover -race ./...
+
+integration-test:
+	psql -U postgres -c 'drop database if exists miniflux_test;'
+	psql -U postgres -c 'create database miniflux_test;'
+	DATABASE_URL=$(DB_URL) go run main.go -migrate
+	DATABASE_URL=$(DB_URL) ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go -create-admin
+	go build -o miniflux-test main.go
+	DATABASE_URL=$(DB_URL) ./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
+	while ! echo exit | nc localhost 8080; do sleep 1; done >/dev/null
+	go test -v -tags=integration || cat /tmp/miniflux.log
+
+clean-integration-test:
+	@ kill -9 `cat /tmp/miniflux.pid`
+	@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
+	@ rm miniflux-test
+	@ psql -U postgres -c 'drop database if exists miniflux_test;'

+ 101 - 0
integration_test.go

@@ -0,0 +1,101 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+// +build integration
+
+package main
+
+import (
+	"testing"
+
+	"github.com/miniflux/miniflux-go"
+)
+
+const (
+	testBaseURL  = "http://127.0.0.1:8080"
+	testUsername = "admin"
+	testPassword = "test123"
+)
+
+func TestGetUsers(t *testing.T) {
+	client := miniflux.NewClient(testBaseURL, testUsername, testPassword)
+	users, err := client.Users()
+	if err != nil {
+		t.Fatal(err)
+		return
+	}
+
+	if len(users) == 0 {
+		t.Fatal("The list of users is empty")
+	}
+
+	if users[0].ID == 0 {
+		t.Fatalf(`Invalid userID, got %v`, users[0].ID)
+	}
+
+	if users[0].Username != testUsername {
+		t.Fatalf(`Invalid username, got %v`, users[0].Username)
+	}
+
+	if users[0].Password != "" {
+		t.Fatalf(`Invalid password, got %v`, users[0].Password)
+	}
+
+	if users[0].Language != "en_US" {
+		t.Fatalf(`Invalid language, got %v`, users[0].Language)
+	}
+
+	if users[0].Theme != "default" {
+		t.Fatalf(`Invalid theme, got %v`, users[0].Theme)
+	}
+
+	if users[0].Timezone != "UTC" {
+		t.Fatalf(`Invalid timezone, got %v`, users[0].Timezone)
+	}
+
+	if !users[0].IsAdmin {
+		t.Fatalf(`Invalid role, got %v`, users[0].IsAdmin)
+	}
+}
+
+func TestCreateStandardUser(t *testing.T) {
+	client := miniflux.NewClient(testBaseURL, testUsername, testPassword)
+	user, err := client.CreateUser("test", "test123", false)
+	if err != nil {
+		t.Fatal(err)
+		return
+	}
+
+	if user.ID == 0 {
+		t.Fatalf(`Invalid userID, got %v`, user.ID)
+	}
+
+	if user.Username != "test" {
+		t.Fatalf(`Invalid username, got %v`, user.Username)
+	}
+
+	if user.Password != "" {
+		t.Fatalf(`Invalid password, got %v`, user.Password)
+	}
+
+	if user.Language != "en_US" {
+		t.Fatalf(`Invalid language, got %v`, user.Language)
+	}
+
+	if user.Theme != "default" {
+		t.Fatalf(`Invalid theme, got %v`, user.Theme)
+	}
+
+	if user.Timezone != "UTC" {
+		t.Fatalf(`Invalid timezone, got %v`, user.Timezone)
+	}
+
+	if user.IsAdmin {
+		t.Fatalf(`Invalid role, got %v`, user.IsAdmin)
+	}
+
+	if user.LastLoginAt != nil {
+		t.Fatalf(`Invalid last login date, got %v`, user.LastLoginAt)
+	}
+}

+ 10 - 2
main.go

@@ -116,8 +116,16 @@ func main() {
 	}
 
 	if *flagCreateAdmin {
-		user := &model.User{IsAdmin: true}
-		user.Username, user.Password = askCredentials()
+		user := &model.User{
+			Username: os.Getenv("ADMIN_USERNAME"),
+			Password: os.Getenv("ADMIN_PASSWORD"),
+			IsAdmin:  true,
+		}
+
+		if user.Username == "" || user.Password == "" {
+			user.Username, user.Password = askCredentials()
+		}
+
 		if err := user.ValidateUserCreation(); err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 5 - 5
model/user.go

@@ -14,11 +14,11 @@ type User struct {
 	ID          int64             `json:"id"`
 	Username    string            `json:"username"`
 	Password    string            `json:"password,omitempty"`
-	IsAdmin     bool              `json:"is_admin"`
-	Theme       string            `json:"theme"`
-	Language    string            `json:"language"`
-	Timezone    string            `json:"timezone"`
-	LastLoginAt *time.Time        `json:"last_login_at"`
+	IsAdmin     bool              `json:"is_admin,omitempty"`
+	Theme       string            `json:"theme,omitempty"`
+	Language    string            `json:"language,omitempty"`
+	Timezone    string            `json:"timezone,omitempty"`
+	LastLoginAt *time.Time        `json:"last_login_at,omitempty"`
 	Extra       map[string]string `json:"-"`
 }
 

+ 12 - 2
storage/user.go

@@ -64,8 +64,18 @@ func (s *Storage) CreateUser(user *model.User) (err error) {
 		}
 	}
 
-	query := "INSERT INTO users (username, password, is_admin, extra) VALUES ($1, $2, $3, $4) RETURNING id"
-	err = s.db.QueryRow(query, strings.ToLower(user.Username), password, user.IsAdmin, extra).Scan(&user.ID)
+	query := `INSERT INTO users
+		(username, password, is_admin, extra)
+		VALUES
+		($1, $2, $3, $4)
+		RETURNING id, language, theme, timezone`
+
+	err = s.db.QueryRow(query, strings.ToLower(user.Username), password, user.IsAdmin, extra).Scan(
+		&user.ID,
+		&user.Language,
+		&user.Theme,
+		&user.Timezone,
+	)
 	if err != nil {
 		return fmt.Errorf("unable to create user: %v", err)
 	}

+ 9 - 0
vendor/github.com/miniflux/miniflux-go/.travis.yml

@@ -0,0 +1,9 @@
+notifications:
+  email: false
+language: go
+go:
+  - 1.9
+before_install:
+  - go get -u github.com/golang/lint/golint
+script:
+  - golint *.go

+ 21 - 0
vendor/github.com/miniflux/miniflux-go/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Frédéric Guillot
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 50 - 0
vendor/github.com/miniflux/miniflux-go/README.md

@@ -0,0 +1,50 @@
+Go Library for Miniflux
+=======================
+[![Build Status](https://travis-ci.org/miniflux/miniflux-go.svg?branch=master)](https://travis-ci.org/miniflux/miniflux-go)
+[![GoDoc](https://godoc.org/github.com/miniflux/miniflux-go?status.svg)](https://godoc.org/github.com/miniflux/miniflux-go)
+
+Client library for Miniflux REST API.
+
+Requirements
+------------
+
+- Miniflux >= 2.0.0
+- Go >= 1.9
+
+Installation
+------------
+
+```bash
+go get -u github.com/miniflux/miniflux-go
+```
+
+Example
+-------
+
+```go
+package main
+
+import (
+	"fmt"
+
+	"github.com/miniflux/miniflux-go"
+)
+
+func main() {
+    client := miniflux.NewClient("https://api.example.org", "admin", "secret")
+
+    // Fetch all feeds.
+    feeds, err := userClient.Feeds()
+    if err != nil {
+        fmt.Println(err)
+        return
+    }
+    fmt.Println(feeds)
+}
+```
+
+Credits
+-------
+
+- Author: Frédéric Guillot
+- Distributed under MIT License

+ 354 - 0
vendor/github.com/miniflux/miniflux-go/client.go

@@ -0,0 +1,354 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the MIT license
+// that can be found in the LICENSE file.
+
+package miniflux
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strconv"
+)
+
+// Client represents a Miniflux client.
+type Client struct {
+	request *request
+}
+
+// Users returns all users.
+func (c *Client) Users() (Users, error) {
+	body, err := c.request.Get("/v1/users")
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var users Users
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&users); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return users, nil
+}
+
+// User returns a single user.
+func (c *Client) User(userID int64) (*User, error) {
+	body, err := c.request.Get(fmt.Sprintf("/v1/users/%d", userID))
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var user User
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&user); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return &user, nil
+}
+
+// CreateUser creates a new user in the system.
+func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, error) {
+	body, err := c.request.Post("/v1/users", &User{Username: username, Password: password, IsAdmin: isAdmin})
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var user *User
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&user); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return user, nil
+}
+
+// UpdateUser updates a user in the system.
+func (c *Client) UpdateUser(user *User) (*User, error) {
+	body, err := c.request.Put(fmt.Sprintf("/v1/users/%d", user.ID), user)
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var u *User
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&u); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return u, nil
+}
+
+// DeleteUser removes a user from the system.
+func (c *Client) DeleteUser(userID int64) error {
+	body, err := c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
+	if err != nil {
+		return err
+	}
+	body.Close()
+	return nil
+}
+
+// Discover try to find subscriptions from a website.
+func (c *Client) Discover(url string) (Subscriptions, error) {
+	body, err := c.request.Post("/v1/discover", map[string]string{"url": url})
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var subscriptions Subscriptions
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&subscriptions); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return subscriptions, nil
+}
+
+// Categories gets the list of categories.
+func (c *Client) Categories() (Categories, error) {
+	body, err := c.request.Get("/v1/categories")
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var categories Categories
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&categories); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return categories, nil
+}
+
+// CreateCategory creates a new category.
+func (c *Client) CreateCategory(title string) (*Category, error) {
+	body, err := c.request.Post("/v1/categories", map[string]interface{}{
+		"title": title,
+	})
+
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var category *Category
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&category); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return category, nil
+}
+
+// UpdateCategory updates a category.
+func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, error) {
+	body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), map[string]interface{}{
+		"title": title,
+	})
+
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var category *Category
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&category); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return category, nil
+}
+
+// DeleteCategory removes a category.
+func (c *Client) DeleteCategory(categoryID int64) error {
+	body, err := c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
+	if err != nil {
+		return err
+	}
+	defer body.Close()
+
+	return nil
+}
+
+// Feeds gets all feeds.
+func (c *Client) Feeds() (Feeds, error) {
+	body, err := c.request.Get("/v1/feeds")
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var feeds Feeds
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&feeds); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return feeds, nil
+}
+
+// Feed gets a new feed.
+func (c *Client) Feed(feedID int64) (*Feed, error) {
+	body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d", feedID))
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var feed *Feed
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&feed); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return feed, nil
+}
+
+// CreateFeed creates a new feed.
+func (c *Client) CreateFeed(url string, categoryID int64) (*Feed, error) {
+	body, err := c.request.Post("/v1/feeds", map[string]interface{}{
+		"feed_url":    url,
+		"category_id": categoryID,
+	})
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var feed *Feed
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&feed); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return feed, nil
+}
+
+// UpdateFeed updates a feed.
+func (c *Client) UpdateFeed(feed *Feed) (*Feed, error) {
+	body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d", feed.ID), feed)
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var f *Feed
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&f); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return f, nil
+}
+
+// RefreshFeed refresh a feed.
+func (c *Client) RefreshFeed(feedID int64) error {
+	body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
+	if err != nil {
+		return err
+	}
+	body.Close()
+	return nil
+}
+
+// DeleteFeed removes a feed.
+func (c *Client) DeleteFeed(feedID int64) error {
+	body, err := c.request.Delete(fmt.Sprintf("/v1/feeds/%d", feedID))
+	if err != nil {
+		return err
+	}
+	body.Close()
+	return nil
+}
+
+// Entry gets a single feed entry.
+func (c *Client) Entry(feedID, entryID int64) (*Entry, error) {
+	body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d/entries/%d", feedID, entryID))
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var entry *Entry
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&entry); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return entry, nil
+}
+
+// Entries gets feed entries.
+func (c *Client) Entries(feedID int64, filter *Filter) (*EntryResultSet, error) {
+	path := fmt.Sprintf("/v1/feeds/%d/entries", feedID)
+
+	if filter != nil {
+		values := url.Values{}
+
+		if filter.Status != "" {
+			values.Set("status", filter.Status)
+		}
+
+		if filter.Direction != "" {
+			values.Set("direction", filter.Direction)
+		}
+
+		if filter.Order != "" {
+			values.Set("order", filter.Order)
+		}
+
+		if filter.Limit != 0 {
+			values.Set("limit", strconv.Itoa(filter.Limit))
+		}
+
+		if filter.Offset != 0 {
+			values.Set("offset", strconv.Itoa(filter.Offset))
+		}
+
+		path = fmt.Sprintf("%s?%s", path, values.Encode())
+	}
+
+	body, err := c.request.Get(path)
+	if err != nil {
+		return nil, err
+	}
+	defer body.Close()
+
+	var result EntryResultSet
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&result); err != nil {
+		return nil, fmt.Errorf("miniflux: response error (%v)", err)
+	}
+
+	return &result, nil
+}
+
+// UpdateEntries updates the status of a list of entries.
+func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
+	type payload struct {
+		EntryIDs []int64 `json:"entry_ids"`
+		Status   string  `json:"status"`
+	}
+
+	body, err := c.request.Put("/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
+	if err != nil {
+		return err
+	}
+	body.Close()
+
+	return nil
+}
+
+// NewClient returns a new Client.
+func NewClient(endpoint, username, password string) *Client {
+	return &Client{request: &request{endpoint: endpoint, username: username, password: password}}
+}

+ 31 - 0
vendor/github.com/miniflux/miniflux-go/doc.go

@@ -0,0 +1,31 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the MIT license
+// that can be found in the LICENSE file.
+
+/*
+
+Package miniflux implements a client library for the Miniflux REST API.
+
+Examples
+
+This code snippet fetch the list of users.
+
+	client := miniflux.NewClient("https://api.example.org", "admin", "secret")
+	users, err := client.Users()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(users, err)
+
+This one discover subscriptions on a website.
+
+	subscriptions, err := client.Discover("https://example.org/")
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(subscriptions)
+
+*/
+package miniflux

+ 131 - 0
vendor/github.com/miniflux/miniflux-go/miniflux.go

@@ -0,0 +1,131 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the MIT license
+// that can be found in the LICENSE file.
+
+package miniflux
+
+import (
+	"fmt"
+	"time"
+)
+
+// Entry statuses.
+const (
+	EntryStatusUnread  = "unread"
+	EntryStatusRead    = "read"
+	EntryStatusRemoved = "removed"
+)
+
+// User represents a user in the system.
+type User struct {
+	ID          int64      `json:"id"`
+	Username    string     `json:"username"`
+	Password    string     `json:"password,omitempty"`
+	IsAdmin     bool       `json:"is_admin"`
+	Theme       string     `json:"theme"`
+	Language    string     `json:"language"`
+	Timezone    string     `json:"timezone"`
+	LastLoginAt *time.Time `json:"last_login_at"`
+}
+
+func (u User) String() string {
+	return fmt.Sprintf("#%d - %s (admin=%v)", u.ID, u.Username, u.IsAdmin)
+}
+
+// Users represents a list of users.
+type Users []User
+
+// Category represents a category in the system.
+type Category struct {
+	ID     int64  `json:"id,omitempty"`
+	Title  string `json:"title,omitempty"`
+	UserID int64  `json:"user_id,omitempty"`
+}
+
+func (c Category) String() string {
+	return fmt.Sprintf("#%d %s", c.ID, c.Title)
+}
+
+// Categories represents a list of categories.
+type Categories []*Category
+
+// Subscription represents a feed subscription.
+type Subscription struct {
+	Title string `json:"title"`
+	URL   string `json:"url"`
+	Type  string `json:"type"`
+}
+
+func (s Subscription) String() string {
+	return fmt.Sprintf(`Title="%s", URL="%s", Type="%s"`, s.Title, s.URL, s.Type)
+}
+
+// Subscriptions represents a list of subscriptions.
+type Subscriptions []*Subscription
+
+// Feed represents a Miniflux feed.
+type Feed struct {
+	ID                 int64     `json:"id"`
+	UserID             int64     `json:"user_id"`
+	FeedURL            string    `json:"feed_url"`
+	SiteURL            string    `json:"site_url"`
+	Title              string    `json:"title"`
+	CheckedAt          time.Time `json:"checked_at,omitempty"`
+	EtagHeader         string    `json:"etag_header,omitempty"`
+	LastModifiedHeader string    `json:"last_modified_header,omitempty"`
+	ParsingErrorMsg    string    `json:"parsing_error_message,omitempty"`
+	ParsingErrorCount  int       `json:"parsing_error_count,omitempty"`
+	Category           *Category `json:"category,omitempty"`
+	Entries            Entries   `json:"entries,omitempty"`
+}
+
+// Feeds represents a list of feeds.
+type Feeds []*Feed
+
+// Entry represents a subscription item in the system.
+type Entry struct {
+	ID         int64      `json:"id"`
+	UserID     int64      `json:"user_id"`
+	FeedID     int64      `json:"feed_id"`
+	Status     string     `json:"status"`
+	Hash       string     `json:"hash"`
+	Title      string     `json:"title"`
+	URL        string     `json:"url"`
+	Date       time.Time  `json:"published_at"`
+	Content    string     `json:"content"`
+	Author     string     `json:"author"`
+	Enclosures Enclosures `json:"enclosures,omitempty"`
+	Feed       *Feed      `json:"feed,omitempty"`
+	Category   *Category  `json:"category,omitempty"`
+}
+
+// Entries represents a list of entries.
+type Entries []*Entry
+
+// Enclosure represents an attachment.
+type Enclosure struct {
+	ID       int64  `json:"id"`
+	UserID   int64  `json:"user_id"`
+	EntryID  int64  `json:"entry_id"`
+	URL      string `json:"url"`
+	MimeType string `json:"mime_type"`
+	Size     int    `json:"size"`
+}
+
+// Enclosures represents a list of attachments.
+type Enclosures []*Enclosure
+
+// Filter is used to filter entries.
+type Filter struct {
+	Status    string
+	Offset    int
+	Limit     int
+	Order     string
+	Direction string
+}
+
+// EntryResultSet represents the response when fetching entries.
+type EntryResultSet struct {
+	Total   int     `json:"total"`
+	Entries Entries `json:"entries"`
+}

+ 136 - 0
vendor/github.com/miniflux/miniflux-go/request.go

@@ -0,0 +1,136 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the MIT license
+// that can be found in the LICENSE file.
+
+package miniflux
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+const (
+	userAgent      = "Miniflux Client Library <https://github.com/miniflux/miniflux-go>"
+	defaultTimeout = 80
+)
+
+var (
+	errNotAuthorized = errors.New("miniflux: unauthorized (bad credentials)")
+	errForbidden     = errors.New("miniflux: access forbidden")
+	errServerError   = errors.New("miniflux: internal server error")
+)
+
+type errorResponse struct {
+	ErrorMessage string `json:"error_message"`
+}
+
+type request struct {
+	endpoint string
+	username string
+	password string
+}
+
+func (r *request) Get(path string) (io.ReadCloser, error) {
+	return r.execute(http.MethodGet, path, nil)
+}
+
+func (r *request) Post(path string, data interface{}) (io.ReadCloser, error) {
+	return r.execute(http.MethodPost, path, data)
+}
+
+func (r *request) Put(path string, data interface{}) (io.ReadCloser, error) {
+	return r.execute(http.MethodPut, path, data)
+}
+
+func (r *request) Delete(path string) (io.ReadCloser, error) {
+	return r.execute(http.MethodDelete, path, nil)
+}
+
+func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
+	u, err := url.Parse(r.endpoint + path)
+	if err != nil {
+		return nil, err
+	}
+
+	request := &http.Request{
+		URL:    u,
+		Method: method,
+		Header: r.buildHeaders(),
+	}
+	request.SetBasicAuth(r.username, r.password)
+
+	if data != nil {
+		request.Body = ioutil.NopCloser(bytes.NewBuffer(r.toJSON(data)))
+	}
+
+	client := r.buildClient()
+	response, err := client.Do(request)
+	if err != nil {
+		return nil, err
+	}
+
+	switch response.StatusCode {
+	case http.StatusUnauthorized:
+		return nil, errNotAuthorized
+	case http.StatusForbidden:
+		return nil, errForbidden
+	case http.StatusInternalServerError:
+		return nil, errServerError
+	case http.StatusBadRequest:
+		defer response.Body.Close()
+
+		var resp errorResponse
+		decoder := json.NewDecoder(response.Body)
+		if err := decoder.Decode(&resp); err != nil {
+			return nil, fmt.Errorf("miniflux: bad request error (%v)", err)
+		}
+
+		return nil, fmt.Errorf("miniflux: bad request (%s)", resp.ErrorMessage)
+	}
+
+	if response.StatusCode >= 400 {
+		return nil, fmt.Errorf("miniflux: server error (statusCode=%d)", response.StatusCode)
+	}
+
+	return response.Body, nil
+}
+
+func (r *request) buildClient() http.Client {
+	return http.Client{
+		Timeout: time.Duration(defaultTimeout * time.Second),
+	}
+}
+
+func (r *request) buildHeaders() http.Header {
+	headers := make(http.Header)
+	headers.Add("User-Agent", userAgent)
+	headers.Add("Content-Type", "application/json")
+	headers.Add("Accept", "application/json")
+	return headers
+}
+
+func (r *request) toJSON(v interface{}) []byte {
+	b, err := json.Marshal(v)
+	if err != nil {
+		log.Println("Unable to convert interface to JSON:", err)
+		return []byte("")
+	}
+
+	return b
+}
+
+func newRequest(endpoint, username, password string) *request {
+	return &request{
+		endpoint: endpoint,
+		username: username,
+		password: password,
+	}
+}