Skip to content

Commit

Permalink
Make a lot of refactoring and get everything work
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillrogovoy committed Sep 23, 2017
1 parent 861d99b commit 8fa155e
Show file tree
Hide file tree
Showing 47 changed files with 1,605 additions and 553 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
worklog.txt
vendor/
63 changes: 63 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cache

import (
"encoding/json"
"fmt"
"os"
"path"
"strings"
)

var cachePath = path.Join(os.TempDir(), "pullk_cache", "cache.json")

// Cache is an interface for a Get/Set caching struct
type Cache interface {
Set(string, interface{}) error
Get(string, interface{}) (bool, error)
}

// FS is an interface for interacting with the file system
type FS interface {
Mkdir(path string, perms os.FileMode) error
WriteFile(path string, data []byte, perms os.FileMode) error
ReadFile(path string) ([]byte, error)
}

// FSCache is a set of methods to write to and read from the cache, transforming JSON in the process
type FSCache struct {
CachePath string
FS FS
}

// Set converts `x` to JSON and writes it to a file using `key`
func (c FSCache) Set(key string, x interface{}) error {
err := c.FS.Mkdir(c.CachePath, 0744)
if err != nil {
return err
}

jsoned, err := json.Marshal(x)
if err != nil {
return err
}

return c.FS.WriteFile(c.filePath(key), jsoned, 0644)
}

// Get gets the contents of a file using `key` (if exists) and Unmarshals it to the struct `x`
func (c FSCache) Get(key string, x interface{}) (bool, error) {
data, err := c.FS.ReadFile(c.filePath(key))
if err != nil {
// "no such file or directory" just should mean there's no cache entry
if strings.Contains(err.Error(), "no such file or directory") {
return false, nil
}
return false, err
}

return true, json.Unmarshal(data, x)
}

func (c FSCache) filePath(key string) string {
return path.Join(c.CachePath, fmt.Sprintf("%s.json", key))
}
175 changes: 175 additions & 0 deletions cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package cache

import (
"fmt"
"log"
"os"
"testing"

"github.com/stretchr/testify/require"
)

func TestWrite(t *testing.T) {
t.Run("Works when successfully JSON-ed and wrote to the file", func(t *testing.T) {
m := mockFS{cache: map[string]cacheEntity{}}
c := FSCache{
FS: &m,
CachePath: "/tmp/",
}
err := c.Set("key1", testStruct{"val1"})

require.Nil(t, err)
require.Equal(t, `{"x":"val1"}`, string(m.cache["/tmp/key1.json"].data))
require.Contains(t, m.mkdirs, "/tmp/")
})

t.Run("Fails when couldn't do Mkdir", func(t *testing.T) {
m := mockFS{
cache: map[string]cacheEntity{},
mkdirErr: fmt.Errorf("Mkdir: fail"),
}
c := FSCache{
FS: &m,
CachePath: "/tmp/",
}
err := c.Set("key1", testStruct{"val1"})

require.EqualError(t, err, "Mkdir: fail")
require.Nil(t, m.cache["/tmp/key1.json"].data)
require.Contains(t, m.mkdirs, "/tmp/")
})

t.Run("Fails when couldn't json.Marshal() the input", func(t *testing.T) {
m := mockFS{cache: map[string]cacheEntity{}}
c := FSCache{
FS: &m,
CachePath: "/tmp/",
}
err := c.Set("key1", func() {}) // functions are unmarshable in Go

log.Println(err)
require.EqualError(t, err, "json: unsupported type: func()")
require.Nil(t, m.cache["/tmp/key1.json"].data)
require.Contains(t, m.mkdirs, "/tmp/")
})

t.Run("Fails when couldn't write to the file", func(t *testing.T) {
m := mockFS{
cache: map[string]cacheEntity{},
writeFileErr: fmt.Errorf("Failed to write to the file"),
}
c := FSCache{
FS: &m,
CachePath: "/tmp/",
}
err := c.Set("key1", testStruct{"val1"})

require.EqualError(t, err, "Failed to write to the file")
require.Nil(t, m.cache["/tmp/key1.json"].data)
require.Contains(t, m.mkdirs, "/tmp/")
})
}

func TestRead(t *testing.T) {
t.Run("Works when the file is readable and is correct JSON", func(t *testing.T) {
s := testStruct{}
m := mockFS{
cache: map[string]cacheEntity{
"/tmp/key1.json": cacheEntity{[]byte(`{"x":"val1"}`), 0777},
},
}
c := FSCache{
FS: &m,
CachePath: "/tmp/",
}
ok, err := c.Get("key1", &s)

require.Nil(t, err)
require.True(t, ok)
require.Equal(t, testStruct{"val1"}, s)
})

t.Run("Fails when couldn't read the file", func(t *testing.T) {
s := testStruct{}
m := mockFS{
readFileErr: fmt.Errorf("Some weird FS error"),
}
c := FSCache{
FS: &m,
CachePath: "/tmp/",
}
ok, err := c.Get("key1", &s)

require.False(t, ok)
require.EqualError(t, err, "Some weird FS error")
})

t.Run("Fails when couldn't unmarshal the contents", func(t *testing.T) {
s := testStruct{}
m := mockFS{
cache: map[string]cacheEntity{
"/tmp/key1.json": cacheEntity{[]byte(`incorrect JSON`), 0777},
},
}
c := FSCache{
FS: &m,
CachePath: "/tmp/",
}
ok, err := c.Get("key1", &s)

require.True(t, ok)
require.Contains(t, err.Error(), "invalid character 'i'")
})

t.Run("Doesn't fail when the file didn't exist", func(t *testing.T) {
s := testStruct{}
m := mockFS{
readFileErr: fmt.Errorf("/tmp/key1.json: no such file or directory"),
}
c := FSCache{
FS: &m,
CachePath: "/tmp/",
}
ok, err := c.Get("key1", &s)

require.False(t, ok)
require.Nil(t, err)
})
}

type cacheEntity struct {
data []byte
perm os.FileMode
}

type mockFS struct {
mkdirs []string
cache map[string]cacheEntity
mkdirErr error
writeFileErr error
readFileErr error
}

func (m *mockFS) Mkdir(path string, perms os.FileMode) error {
m.mkdirs = append(m.mkdirs, path)
return m.mkdirErr
}

func (m *mockFS) WriteFile(key string, data []byte, perm os.FileMode) error {
if m.writeFileErr == nil {
m.cache[key] = cacheEntity{data, perm}
}
return m.writeFileErr
}

func (m *mockFS) ReadFile(key string) ([]byte, error) {
entity, ok := m.cache[key]
if !ok {
return nil, m.readFileErr
}
return entity.data, m.readFileErr
}

type testStruct struct {
X string `json:"x"`
}
19 changes: 9 additions & 10 deletions cmd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"regexp"
"strings"

"github.com/kirillrogovoy/pullk/github"
client "github.com/kirillrogovoy/pullk/github/client"
)

const usage = `Usage:
Expand All @@ -16,7 +16,6 @@ const usage = `Usage:
Flags:
--limit - Only use N last pull requests
--reset - Reset the local cache and fetch all the data through the Github API again
Environment variables:
GITHUB_CREDS - API credentials in the format "username:personal_access_token"`
Expand All @@ -40,22 +39,22 @@ func getFlags() flags {
return flags
}

func getGithubCreds() *github.Credentials {
env := os.Getenv("GITHUB_CREDS")
func getGithubCreds() *client.Credentials {
creds := os.Getenv("GITHUB_CREDS")

if env == "" {
if creds == "" {
return nil
}

if matches, _ := regexp.MatchString(`^[\w-]+:[\w-]+$`, env); !matches {
if matches, _ := regexp.MatchString(`^[\w-]+:[\w-]+$`, creds); !matches {
fmt.Printf("Invalid format of the GITHUB_CREDS environment variable!\n\n%s\n", usage)
os.Exit(1)
}

creds := strings.Split(env, ":")
return &github.Credentials{
Username: creds[0],
PersonalAccessToken: creds[1],
split := strings.Split(creds, ":")
return &client.Credentials{
Username: split[0],
PersonalAccessToken: split[1],
}
}

Expand Down
18 changes: 18 additions & 0 deletions cmd/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import (
"io/ioutil"
"os"
)

type RealFS struct{}

func (f RealFS) Mkdir(path string, perms os.FileMode) error {
return os.MkdirAll(path, perms)
}
func (f RealFS) WriteFile(path string, data []byte, perms os.FileMode) error {
return ioutil.WriteFile(path, data, perms)
}
func (f RealFS) ReadFile(path string) ([]byte, error) {
return ioutil.ReadFile(path)
}
Loading

0 comments on commit 8fa155e

Please sign in to comment.