Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
aa5362c
Split models into separate repository files
vaihtovirta May 9, 2020
62bd09b
Create UserRepository
vaihtovirta May 10, 2020
78979d1
Create TimecodeRepository
vaihtovirta May 10, 2020
8f4e4bf
Create TimecodeLikeRepository
vaihtovirta May 10, 2020
559d56f
Define Container for DI
vaihtovirta May 10, 2020
18211a2
Update auth middleware to accept container
vaihtovirta May 10, 2020
f4226b0
Implement container-aware route handlers
vaihtovirta May 10, 2020
ba5b133
Add unit tests for google_api#FetchUserInfo
vaihtovirta May 10, 2020
768410e
Add unit tests for youtube_api package
vaihtovirta May 10, 2020
34c0041
Refactor go routine for creating timecodes from yt video
vaihtovirta May 10, 2020
da29b9a
Set up test database
vaihtovirta May 10, 2020
268d4a5
Add tests for UserRepository
vaihtovirta May 10, 2020
b967474
Update README
vaihtovirta May 10, 2020
7673d8f
Use docker-compose for tests
vaihtovirta May 10, 2020
4bf580c
Fix linter errors
vaihtovirta May 10, 2020
c6a151f
Remove debug prints
vaihtovirta May 11, 2020
984db8b
Register test db only once
vaihtovirta May 12, 2020
787d8d0
Use suite in UserRepository tests
vaihtovirta May 12, 2020
4dfc798
Add tests for TimecodeRepository
vaihtovirta May 12, 2020
795dabf
Fix repository tests
vaihtovirta May 16, 2020
c219590
Add tests for some router methods
vaihtovirta May 19, 2020
b4165e5
Fix golangci-lint issues
vaihtovirta May 19, 2020
d3b7051
Add test coverage report into PR checks
vaihtovirta May 19, 2020
565f2b5
Fix coveralls action
vaihtovirta May 19, 2020
1155280
Add coveralls badge to README
vaihtovirta May 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .air.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ cmd = "go build -o ./tmp/main ./cmd"
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
full_bin = "APP_ENV=development APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go"]
# Ignore these filename extNensions or directories.
Expand Down
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
APP_ENV=development
PORT=8080
PG_USER=postgres
PG_PASSWORD=postgres
PG_HOST=db
PG_PORT=5432
PG_DB=timecodes_db

POSTGRES_DB=timecodes_development

GOOGLE_API_KEY=
10 changes: 10 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
APP_ENV=test
PORT=8080
PG_USER=postgres
PG_PASSWORD=postgres
PG_HOST=db
PG_PORT=5432

POSTGRES_DB=timecodes_test

GOOGLE_API_KEY=
28 changes: 16 additions & 12 deletions .github/workflows/pr_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ jobs:
steps:
- name: Checkout current branch
uses: actions/checkout@master
- name: Cache
uses: actions/cache@v1
- name: docker-compose version
run: docker-compose --version
- name: Build docker image
run: |
cp .env.example .env
docker-compose build app_test
- name: Run tests
run: docker-compose run app_test go test ./... -covermode=count -coverprofile=coverage.out
- name: Convert coverage to lcov
uses: jandelgado/gcov2lcov-action@v1.0.2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- uses: actions/setup-go@v1
infile: coverage.out
outfile: coverage.lcov
- name: Coveralls
uses: coverallsapp/github-action@master
with:
go-version: '1.13'
- name: Run go test
run: |
go mod download
go test ./... -cover
github-token: ${{ secrets.github_token }}
path-to-lcov: coverage.lcov
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
# Backend for Timecodes Chrome extension

[![Coverage Status](https://coveralls.io/repos/github/letscode-io/timecodes-api/badge.svg)](https://coveralls.io/github/letscode-io/timecodes-api)

# Development

```bash
$ docker-compose up --build app_dev
```

# Run the whole test suite

```bash
$ docker-compose build app_test
$ docker-compose run app_test go test ./... -cover
```

# Run a single test

```bash
$ docker-compose build app_test
$ docker-compose run app_test go test -run Test%TEST_FUNCTION_NAME%
```

# Build for using in production

```bash
Expand Down
50 changes: 25 additions & 25 deletions cmd/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,31 @@ var authTokenRegExp = regexp.MustCompile(`Bearer (\S+$)`)

type CurrentUserKey struct{}

func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := getAuthToken(r.Header.Get("Authorization"))

userInfo, err := googleAPI.FetchUserInfo(token)
if err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}

ctx := context.WithValue(r.Context(), CurrentUserKey{}, findOrCreateUser(userInfo))
r = r.WithContext(ctx)

next.ServeHTTP(w, r)
})
func authMiddleware(c *Container) (mw func(http.Handler) http.Handler) {
mw = func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
currentUser := getCurrentUser(r)
if currentUser != nil {
next.ServeHTTP(w, r)
return
}

token := getAuthToken(r.Header.Get("Authorization"))

userInfo, err := googleAPI.FetchUserInfo(token)
if err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}

user := c.UserRepository.FindOrCreateByGoogleInfo(userInfo)
ctx := context.WithValue(r.Context(), CurrentUserKey{}, user)
r = r.WithContext(ctx)

next.ServeHTTP(w, r)
})
}
return
}

func getAuthToken(authorizationHeader string) string {
Expand All @@ -40,13 +50,3 @@ func getAuthToken(authorizationHeader string) string {

return string(token)
}

func findOrCreateUser(userInfo *googleAPI.UserInfo) *User {
currentUser := &User{}

db.Where(User{GoogleID: userInfo.Id}).
Assign(User{Email: userInfo.Email, PictureURL: userInfo.Picture}).
FirstOrCreate(&currentUser)

return currentUser
}
13 changes: 13 additions & 0 deletions cmd/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

import (
youtubeapi "timecodes/cmd/youtube_api"
)

type Container struct {
TimecodeLikeRepository TimecodeLikeRepository
TimecodeRepository TimecodeRepository
UserRepository UserRepository

YoutubeAPI *youtubeapi.Service
}
80 changes: 50 additions & 30 deletions cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,67 @@ import (
_ "github.com/jinzhu/gorm/dialects/postgres"
)

func initDB() {
var err error
func getDbName() string {
return fmt.Sprintf("timecodes_%s", os.Getenv("APP_ENV"))
}

dsn := url.URL{
User: url.UserPassword(os.Getenv("PG_USER"), os.Getenv("PG_PASSWORD")),
Scheme: "postgres",
Host: fmt.Sprintf("%s:%s", os.Getenv("PG_HOST"), os.Getenv("PG_PORT")),
Path: os.Getenv("PG_DB"),
RawQuery: (&url.Values{"sslmode": []string{"disable"}}).Encode(),
}
func getEnvDSN() url.URL {
return getDsn(getDbName())
}

db, err = gorm.Open("postgres", dsn.String())
func initDB() *gorm.DB {
envDsn := getEnvDSN()

db, err := gorm.Open("postgres", envDsn.String())
if err != nil {
log.Println("Failed to connect to database")
panic(err)
dbName := getDbName()
db = createDatabase(dbName)
}

log.Println("DB connection has been established.")
log.Println("Connection to the database has been established.")

return db
}

func createTables() {
if db.HasTable(&Timecode{}) {
return
func createDatabase(dbName string) *gorm.DB {
db := getDefaultConnection()

err := db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbName)).Error
if err != nil {
handleDBConnectionError(err)
}

err := db.CreateTable(&Timecode{})
dsn := getDsn(dbName)

db, err = gorm.Open("postgres", dsn.String())
if err != nil {
log.Println("Table already exists")
handleDBConnectionError(err)
}

return db
}

func runMigrations() {
db.AutoMigrate(&Timecode{})
db.Model(&Timecode{}).AddUniqueIndex(
"idx_timecodes_seconds_text_video_id",
"seconds", "description", "video_id",
)
db.AutoMigrate(&User{})
db.AutoMigrate(&TimecodeLike{})
db.Model(&TimecodeLike{}).AddUniqueIndex(
"idx_timecodes_likes_user_id_timecode_id_video_id",
"user_id", "timecode_id",
)
func getDefaultConnection() *gorm.DB {
defaultDsn := getDsn("postgres")
db, err := gorm.Open("postgres", defaultDsn.String())
if err != nil {
handleDBConnectionError(err)
}

return db
}

func handleDBConnectionError(err error) {
log.Println("Unable to connect to db")
panic(err)
}

func getDsn(path string) url.URL {
return url.URL{
User: url.UserPassword(os.Getenv("PG_USER"), os.Getenv("PG_PASSWORD")),
Scheme: "postgres",
Host: fmt.Sprintf("%s:%s", os.Getenv("PG_HOST"), os.Getenv("PG_PORT")),
Path: path,
RawQuery: (&url.Values{"sslmode": []string{"disable"}}).Encode(),
}
}
35 changes: 35 additions & 0 deletions cmd/db_migrations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import "github.com/jinzhu/gorm"

func runMigrations(db *gorm.DB) {
applyTimecodesMigrations(db)
applyUsersMigrations(db)
applyTimecodeLikesMigrations(db)
}

func applyTimecodesMigrations(db *gorm.DB) {
db.AutoMigrate(&Timecode{})
db.Model(&Timecode{}).AddUniqueIndex(
"idx_timecodes_seconds_text_video_id",
"seconds", "description", "video_id",
)
db.Exec(`
ALTER TABLE timecodes
ADD CONSTRAINT description_min_length CHECK (length(description) >= 1);
`)
}

func applyUsersMigrations(db *gorm.DB) {
db.AutoMigrate(&User{})
}

func applyTimecodeLikesMigrations(db *gorm.DB) {
db.AutoMigrate(&TimecodeLike{})
db.Model(&TimecodeLike{}).AddUniqueIndex(
"idx_timecodes_likes_user_id_timecode_id_video_id",
"user_id", "timecode_id",
)
db.Model(&TimecodeLike{}).AddForeignKey("timecode_id", "timecodes(id)", "RESTRICT", "RESTRICT")
db.Model(&TimecodeLike{}).AddForeignKey("user_id", "users(id)", "RESTRICT", "RESTRICT")
}
20 changes: 18 additions & 2 deletions cmd/google_api/google_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ import (
const userInfoHost = "https://www.googleapis.com/oauth2/v2/userinfo"

type UserInfo struct {
Id string `json:"id"`
ID string `json:"id"`
Picture string `json:"picture"`
Email string `json:"email"`
}

type APIError struct {
ErrorData struct {
Message string `json:"message"`
} `json:"error"`
}

func (e *APIError) Error() string {
return e.ErrorData.Message
}

func FetchUserInfo(accessToken string) (userInfo *UserInfo, err error) {
if len(accessToken) == 0 {
return nil, errors.New("accessToken cannot be empty")
Expand All @@ -35,7 +45,13 @@ func FetchUserInfo(accessToken string) (userInfo *UserInfo, err error) {

switch response.StatusCode {
case 401:
userInfo, err = nil, errors.New(string(contents))
apiError := &APIError{}
unmarshalErr := json.Unmarshal(contents, apiError)
if unmarshalErr != nil {
return nil, unmarshalErr
}

err = apiError
default:
userInfo = &UserInfo{}
err = nil
Expand Down
Loading