commit
aba50767b6
2
.github/workflows/go.yml
vendored
2
.github/workflows/go.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -22,6 +22,9 @@ jobs:
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
-
|
||||
name: Test
|
||||
run: go test -v ./...
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
|
1
go.mod
1
go.mod
@ -5,5 +5,6 @@ go 1.12
|
||||
require (
|
||||
github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/tools v0.0.0-20170217234718-8e779ee0a450
|
||||
)
|
||||
|
11
go.sum
11
go.sum
@ -1,6 +1,17 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939 h1:yZJImkCmVI6d1uJ9KRRf/96YbFLDQ/hhs6Xt9Z3OBXI=
|
||||
github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/tools v0.0.0-20170217234718-8e779ee0a450 h1:qbbvkCEu5ZgZKpHV38z/uXcloRX6fn/EgiGMW/9eluc=
|
||||
golang.org/x/tools v0.0.0-20170217234718-8e779ee0a450/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
158
internal/service/service.go
Normal file
158
internal/service/service.go
Normal file
@ -0,0 +1,158 @@
|
||||
//package service provides a http handler that reads the path in the request.url and returns
|
||||
// an xml document that follows the OPDS 1.1 standard
|
||||
// https://specs.opds.io/opds-1.1.html
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/dubyte/dir2opds/opds"
|
||||
"golang.org/x/tools/blog/atom"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_ = mime.AddExtensionType(".mobi", "application/x-mobipocket-ebook")
|
||||
_ = mime.AddExtensionType(".epub", "application/epub+zip")
|
||||
_ = mime.AddExtensionType(".cbz", "application/x-cbz")
|
||||
_ = mime.AddExtensionType(".cbr", "application/x-cbr")
|
||||
_ = mime.AddExtensionType(".fb2", "text/fb2+xml")
|
||||
}
|
||||
|
||||
type OPDS struct {
|
||||
DirRoot string
|
||||
Author string
|
||||
AuthorEmail string
|
||||
AuthorURI string
|
||||
}
|
||||
|
||||
var TimeNowFunc = timeNow
|
||||
|
||||
func (s OPDS) Handler(w http.ResponseWriter, req *http.Request) error {
|
||||
fPath := path.Join(s.DirRoot, req.URL.Path)
|
||||
|
||||
log.Printf("fPath:'%s'", fPath)
|
||||
|
||||
fi, err := os.Stat(fPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isFile(fi) {
|
||||
http.ServeFile(w, req, fPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := s.getContent(req, fPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content = append([]byte(xml.Header), content...)
|
||||
http.ServeContent(w, req, "feed.xml", TimeNowFunc(), bytes.NewReader(content))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s OPDS) getContent(req *http.Request, dirpath string) (result []byte, err error) {
|
||||
feed := s.makeFeed(dirpath, req)
|
||||
if getPathType(dirpath) == pathTypeDirOfFiles {
|
||||
acFeed := &opds.AcquisitionFeed{&feed, "http://purl.org/dc/terms/", "http://opds-spec.org/2010/catalog"}
|
||||
result, err = xml.MarshalIndent(acFeed, " ", " ")
|
||||
} else {
|
||||
result, err = xml.MarshalIndent(feed, " ", " ")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const navigationType = "application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
|
||||
func (s OPDS) makeFeed(dirpath string, req *http.Request) atom.Feed {
|
||||
feedBuilder := opds.FeedBuilder.
|
||||
ID(req.URL.Path).
|
||||
Title("Catalog in " + req.URL.Path).
|
||||
Author(opds.AuthorBuilder.Name(s.Author).Email(s.AuthorEmail).URI(s.AuthorURI).Build()).
|
||||
Updated(TimeNowFunc()).
|
||||
AddLink(opds.LinkBuilder.Rel("start").Href("/").Type(navigationType).Build())
|
||||
|
||||
fis, _ := ioutil.ReadDir(dirpath)
|
||||
for _, fi := range fis {
|
||||
pathType := getPathType(path.Join(dirpath, fi.Name()))
|
||||
feedBuilder = feedBuilder.
|
||||
AddEntry(opds.EntryBuilder.
|
||||
ID(req.URL.Path + fi.Name()).
|
||||
Title(fi.Name()).
|
||||
Updated(TimeNowFunc()).
|
||||
Published(TimeNowFunc()).
|
||||
AddLink(opds.LinkBuilder.
|
||||
Rel(getRel(fi.Name(), pathType)).
|
||||
Title(fi.Name()).
|
||||
Href(getHref(req, fi.Name())).
|
||||
Type(getType(fi.Name(), pathType)).
|
||||
Build()).
|
||||
Build())
|
||||
}
|
||||
return feedBuilder.Build()
|
||||
}
|
||||
|
||||
func getRel(name string, pathType int) string {
|
||||
if pathType == pathTypeDirOfFiles || pathType == pathTypeDirOfDirs {
|
||||
return "subsection"
|
||||
}
|
||||
|
||||
ext := filepath.Ext(name)
|
||||
if ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" {
|
||||
return "http://opds-spec.org/image/thumbnail"
|
||||
}
|
||||
|
||||
// mobi, epub, etc
|
||||
return "http://opds-spec.org/acquisition"
|
||||
}
|
||||
|
||||
func getType(name string, pathType int) string {
|
||||
if pathType == pathTypeFile {
|
||||
return mime.TypeByExtension(filepath.Ext(name))
|
||||
}
|
||||
return "application/atom+xml;profile=opds-catalog;kind=acquisition"
|
||||
}
|
||||
|
||||
func getHref(req *http.Request, name string) string {
|
||||
return path.Join(req.URL.RequestURI(), name)
|
||||
}
|
||||
|
||||
const (
|
||||
pathTypeFile = iota
|
||||
pathTypeDirOfDirs
|
||||
pathTypeDirOfFiles
|
||||
)
|
||||
|
||||
func getPathType(dirpath string) int {
|
||||
fi, _ := os.Stat(dirpath)
|
||||
if isFile(fi) {
|
||||
return pathTypeFile
|
||||
}
|
||||
|
||||
fis, _ := ioutil.ReadDir(dirpath)
|
||||
for _, fi := range fis {
|
||||
if isFile(fi) {
|
||||
return pathTypeDirOfFiles
|
||||
}
|
||||
}
|
||||
// Directory of directories
|
||||
return pathTypeDirOfDirs
|
||||
}
|
||||
|
||||
func isFile(fi os.FileInfo) bool {
|
||||
return !fi.IsDir()
|
||||
}
|
||||
|
||||
func timeNow() time.Time {
|
||||
return time.Now()
|
||||
}
|
115
internal/service/service_test.go
Normal file
115
internal/service/service_test.go
Normal file
@ -0,0 +1,115 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dubyte/dir2opds/internal/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
// pre-setup
|
||||
nowFn := service.TimeNowFunc
|
||||
defer func() {
|
||||
service.TimeNowFunc = nowFn
|
||||
}()
|
||||
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
want string
|
||||
WantedContentType string
|
||||
}{
|
||||
"feed (dir of folders )": {input: "/", want: feed, WantedContentType: "application/xml"},
|
||||
"acquisitionFeed(dir of files)": {input: "/mybook", want: acquisitionFeed, WantedContentType: "application/xml"},
|
||||
"servingAFile": {input: "/mybook/mybook.txt", want: "Fixture", WantedContentType: "text/plain; charset=utf-8"},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// setup
|
||||
s := service.OPDS{"testdata", "", "", ""}
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, tc.input, nil)
|
||||
service.TimeNowFunc = func() time.Time {
|
||||
return time.Date(2020, 05, 25, 00, 00, 00, 0, time.UTC)
|
||||
}
|
||||
|
||||
// act
|
||||
err := s.Handler(w, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// post act
|
||||
resp := w.Result()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
assert.Equal(t, tc.WantedContentType, resp.Header.Get("Content-Type"))
|
||||
assert.Equal(t, tc.want, string(body))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var feed = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Catalog in /</title>
|
||||
<id>/</id>
|
||||
<link rel="start" href="/" type="application/atom+xml;profile=opds-catalog;kind=navigation"></link>
|
||||
<updated>2020-05-25T00:00:00+00:00</updated>
|
||||
<author>
|
||||
<name></name>
|
||||
</author>
|
||||
<entry>
|
||||
<title>emptyFolder</title>
|
||||
<id>/emptyFolder</id>
|
||||
<link rel="subsection" href="/emptyFolder" type="application/atom+xml;profile=opds-catalog;kind=acquisition" title="emptyFolder"></link>
|
||||
<published>2020-05-25T00:00:00+00:00</published>
|
||||
<updated>2020-05-25T00:00:00+00:00</updated>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>mybook</title>
|
||||
<id>/mybook</id>
|
||||
<link rel="subsection" href="/mybook" type="application/atom+xml;profile=opds-catalog;kind=acquisition" title="mybook"></link>
|
||||
<published>2020-05-25T00:00:00+00:00</published>
|
||||
<updated>2020-05-25T00:00:00+00:00</updated>
|
||||
</entry>
|
||||
</feed>`
|
||||
|
||||
var acquisitionFeed = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/terms/" xmlns:opds="http://opds-spec.org/2010/catalog">
|
||||
<title>Catalog in /mybook</title>
|
||||
<id>/mybook</id>
|
||||
<link rel="start" href="/" type="application/atom+xml;profile=opds-catalog;kind=navigation"></link>
|
||||
<updated>2020-05-25T00:00:00+00:00</updated>
|
||||
<author>
|
||||
<name></name>
|
||||
</author>
|
||||
<entry>
|
||||
<title>mybook.epub</title>
|
||||
<id>/mybookmybook.epub</id>
|
||||
<link rel="http://opds-spec.org/acquisition" href="/mybook/mybook.epub" type="application/epub+zip" title="mybook.epub"></link>
|
||||
<published>2020-05-25T00:00:00+00:00</published>
|
||||
<updated>2020-05-25T00:00:00+00:00</updated>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>mybook.pdf</title>
|
||||
<id>/mybookmybook.pdf</id>
|
||||
<link rel="http://opds-spec.org/acquisition" href="/mybook/mybook.pdf" type="application/pdf" title="mybook.pdf"></link>
|
||||
<published>2020-05-25T00:00:00+00:00</published>
|
||||
<updated>2020-05-25T00:00:00+00:00</updated>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>mybook.txt</title>
|
||||
<id>/mybookmybook.txt</id>
|
||||
<link rel="http://opds-spec.org/acquisition" href="/mybook/mybook.txt" type="text/plain; charset=utf-8" title="mybook.txt"></link>
|
||||
<published>2020-05-25T00:00:00+00:00</published>
|
||||
<updated>2020-05-25T00:00:00+00:00</updated>
|
||||
</entry>
|
||||
</feed>`
|
BIN
internal/service/testdata/mybook/mybook.epub
vendored
Normal file
BIN
internal/service/testdata/mybook/mybook.epub
vendored
Normal file
Binary file not shown.
BIN
internal/service/testdata/mybook/mybook.pdf
vendored
Normal file
BIN
internal/service/testdata/mybook/mybook.pdf
vendored
Normal file
Binary file not shown.
1
internal/service/testdata/mybook/mybook.txt
vendored
Normal file
1
internal/service/testdata/mybook/mybook.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
Fixture
|
145
main.go
145
main.go
@ -18,21 +18,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/dubyte/dir2opds/opds"
|
||||
"golang.org/x/tools/blog/atom"
|
||||
"github.com/dubyte/dir2opds/internal/service"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -45,20 +37,6 @@ var (
|
||||
debug = flag.Bool("debug", false, "If it is set it will log the requests")
|
||||
)
|
||||
|
||||
type acquisitionFeed struct {
|
||||
*atom.Feed
|
||||
Dc string `xml:"xmlns:dc,attr"`
|
||||
Opds string `xml:"xmlns:opds,attr"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
_ = mime.AddExtensionType(".mobi", "application/x-mobipocket-ebook")
|
||||
_ = mime.AddExtensionType(".epub", "application/epub+zip")
|
||||
_ = mime.AddExtensionType(".cbz", "application/x-cbz")
|
||||
_ = mime.AddExtensionType(".cbr", "application/x-cbr")
|
||||
_ = mime.AddExtensionType(".fb2", "text/fb2+xml")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
flag.Parse()
|
||||
@ -69,7 +47,9 @@ func main() {
|
||||
|
||||
fmt.Println(startValues())
|
||||
|
||||
http.HandleFunc("/", errorHandler(handler))
|
||||
s := service.OPDS{DirRoot: *dirRoot, Author: *author, AuthorEmail: *authorEmail, AuthorURI: *authorURI}
|
||||
|
||||
http.HandleFunc("/", errorHandler(s.Handler))
|
||||
|
||||
log.Fatal(http.ListenAndServe(*host+":"+*port, nil))
|
||||
}
|
||||
@ -79,123 +59,6 @@ func startValues() string {
|
||||
return result
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, req *http.Request) error {
|
||||
fPath := path.Join(*dirRoot, req.URL.Path)
|
||||
|
||||
log.Printf("fPath:'%s'", fPath)
|
||||
|
||||
fi, err := os.Stat(fPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isFile(fi) {
|
||||
http.ServeFile(w, req, fPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := getContent(req, fPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content = append([]byte(xml.Header), content...)
|
||||
http.ServeContent(w, req, "feed.xml", time.Now(), bytes.NewReader(content))
|
||||
return nil
|
||||
}
|
||||
|
||||
func getContent(req *http.Request, dirpath string) (result []byte, err error) {
|
||||
feed := makeFeed(dirpath, req)
|
||||
if getPathType(dirpath) == pathTypeDirOfFiles {
|
||||
acFeed := &acquisitionFeed{&feed, "http://purl.org/dc/terms/", "http://opds-spec.org/2010/catalog"}
|
||||
result, err = xml.MarshalIndent(acFeed, " ", " ")
|
||||
} else {
|
||||
result, err = xml.MarshalIndent(feed, " ", " ")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const navigationType = "application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
|
||||
func makeFeed(dirpath string, req *http.Request) atom.Feed {
|
||||
feedBuilder := opds.FeedBuilder.
|
||||
ID(req.URL.Path).
|
||||
Title("Catalog in " + req.URL.Path).
|
||||
Author(opds.AuthorBuilder.Name(*author).Email(*authorEmail).URI(*authorURI).Build()).
|
||||
Updated(time.Now()).
|
||||
AddLink(opds.LinkBuilder.Rel("start").Href("/").Type(navigationType).Build())
|
||||
|
||||
fis, _ := ioutil.ReadDir(dirpath)
|
||||
for _, fi := range fis {
|
||||
pathType := getPathType(path.Join(dirpath, fi.Name()))
|
||||
feedBuilder = feedBuilder.
|
||||
AddEntry(opds.EntryBuilder.
|
||||
ID(req.URL.Path + fi.Name()).
|
||||
Title(fi.Name()).
|
||||
Updated(time.Now()).
|
||||
Published(time.Now()).
|
||||
AddLink(opds.LinkBuilder.
|
||||
Rel(getRel(fi.Name(), pathType)).
|
||||
Title(fi.Name()).
|
||||
Href(getHref(req, fi.Name())).
|
||||
Type(getType(fi.Name(), pathType)).
|
||||
Build()).
|
||||
Build())
|
||||
}
|
||||
return feedBuilder.Build()
|
||||
}
|
||||
|
||||
func getRel(name string, pathType int) string {
|
||||
if pathType == pathTypeDirOfFiles || pathType == pathTypeDirOfDirs {
|
||||
return "subsection"
|
||||
}
|
||||
|
||||
ext := filepath.Ext(name)
|
||||
if ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" {
|
||||
return "http://opds-spec.org/image/thumbnail"
|
||||
}
|
||||
|
||||
// mobi, epub, etc
|
||||
return "http://opds-spec.org/acquisition"
|
||||
}
|
||||
|
||||
func getType(name string, pathType int) string {
|
||||
if pathType == pathTypeFile {
|
||||
return mime.TypeByExtension(filepath.Ext(name))
|
||||
}
|
||||
return "application/atom+xml;profile=opds-catalog;kind=acquisition"
|
||||
}
|
||||
|
||||
func getHref(req *http.Request, name string) string {
|
||||
return path.Join(req.URL.RequestURI(), name)
|
||||
}
|
||||
|
||||
const (
|
||||
pathTypeFile = iota
|
||||
pathTypeDirOfDirs
|
||||
pathTypeDirOfFiles
|
||||
)
|
||||
|
||||
func getPathType(dirpath string) int {
|
||||
fi, _ := os.Stat(dirpath)
|
||||
if isFile(fi) {
|
||||
return pathTypeFile
|
||||
}
|
||||
|
||||
fis, _ := ioutil.ReadDir(dirpath)
|
||||
for _, fi := range fis {
|
||||
if isFile(fi) {
|
||||
return pathTypeDirOfFiles
|
||||
}
|
||||
}
|
||||
// Directory of directories
|
||||
return pathTypeDirOfDirs
|
||||
}
|
||||
|
||||
func isFile(fi os.FileInfo) bool {
|
||||
return !fi.IsDir()
|
||||
}
|
||||
|
||||
func errorHandler(f func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := f(w, r)
|
||||
|
57
main_test.go
Normal file
57
main_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartValues(t *testing.T) {
|
||||
// pre-setup
|
||||
oldHost, oldPort := *host, *port
|
||||
defer func() {
|
||||
*host = oldHost
|
||||
*port = oldPort
|
||||
}()
|
||||
|
||||
// setup
|
||||
*host = "wow.com"
|
||||
*port = "42"
|
||||
|
||||
// act
|
||||
res := startValues()
|
||||
|
||||
// assert
|
||||
assert.Equal(t, "listening in: wow.com:42", res)
|
||||
|
||||
}
|
||||
|
||||
func TestErrorHandler(t *testing.T) {
|
||||
// pre-setup
|
||||
stdOutput := log.Writer()
|
||||
defer func() {
|
||||
log.SetOutput(stdOutput)
|
||||
}()
|
||||
|
||||
// setup
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf) // to record what is logged
|
||||
|
||||
f := func(http.ResponseWriter, *http.Request) error {
|
||||
return errors.New("scary error")
|
||||
}
|
||||
h := errorHandler(f)
|
||||
res := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
// act
|
||||
h(res, req)
|
||||
|
||||
// assert
|
||||
assert.Contains(t, buf.String(), `handling "/": scary error`)
|
||||
}
|
@ -7,6 +7,12 @@ import (
|
||||
"golang.org/x/tools/blog/atom"
|
||||
)
|
||||
|
||||
type AcquisitionFeed struct {
|
||||
*atom.Feed
|
||||
Dc string `xml:"xmlns:dc,attr"`
|
||||
Opds string `xml:"xmlns:opds,attr"`
|
||||
}
|
||||
|
||||
type feedBuilder builder.Builder
|
||||
|
||||
func (f feedBuilder) Title(title string) feedBuilder {
|
||||
|
Loading…
Reference in New Issue
Block a user