Skip to content

Commit

Permalink
Add API test for creating roadmap
Browse files Browse the repository at this point in the history
  • Loading branch information
peteraba committed Apr 29, 2020
1 parent ba57948 commit 9804b62
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .air.conf
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ exclude_dir = ["airtmp", "build", "data", "docker", "res", "static", "tmp"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = ["cmd/roadmapper/e2e_test.go"]
exclude_file = ["cmd/roadmapper/browser_test.go", "cmd/roadmapper/api_test.go"]
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop to run old binary when build errors occur.
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ integration:
go test -race -tags=integration $(PACKAGES)

e2e:
go test -race -tags=e2e,integration ./...
go test -race -tags=browser,api,integration ./...

codecov:
ifndef CODECOV_TOKEN
Expand All @@ -42,7 +42,7 @@ endif
go test -race -coverprofile=coverage.txt -covermode=atomic $(PACKAGES)
./b.sh -c -F go_unittests
# Code coverage for All tests
go test -race -coverprofile=coverage.txt -covermode=atomic -tags=e2e,integration ./...
go test -race -coverprofile=coverage.txt -covermode=atomic -tags=browser,api,integration ./...
./b.sh -c -F alltests
rm -f b.sh

Expand Down
1 change: 1 addition & 0 deletions api.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"components":{"schemas":{"Dates":{"properties":{"end_at":{"type":"string"},"start_at":{"type":"string"}},"required":["start_at","end_at"]},"Milestone":{"properties":{"title":{"type":"string"},"urls":{"items":{"type":"string"},"type":"array"}},"required":["title"]},"Problem":{"properties":{"detail":{"description":"A human readable explanation specific to this occurrence of the\nproblem.\n","example":"Connection to database timed out","type":"string"},"instance":{"description":"An absolute URI that identifies the specific occurrence of the problem.\nIt may or may not yield further information if dereferenced.\n","format":"uri","type":"string"},"status":{"description":"The HTTP status code generated by the origin server for this occurrence\nof the problem.\n","example":503,"exclusiveMaximum":true,"format":"int32","maximum":600,"minimum":100,"type":"integer"},"title":{"description":"A short, summary of the problem type. Written in English and readable\nfor engineers (usually not suited for non technical stakeholders and\nnot localized); example: Service Unavailable\n","type":"string"},"type":{"default":"about:blank","description":"An absolute URI that identifies the problem type. When dereferenced,\nit SHOULD provide human-readable documentation for the problem type\n(e.g., using HTML).\n","example":"https://rdmp.app/problem/constraint-violation","format":"uri","type":"string"}},"required":["type","title"]},"Project":{"properties":{"dates":{"$ref":"#/components/schemas/Dates"},"indentation":{"minimum":0,"type":"integer"},"milestone":{"minimum":1,"type":"integer"},"percentage":{"maximum":100,"minimum":0,"type":"integer"},"title":{"type":"string"},"urls":{"items":{"type":"string"},"type":"array"}},"required":["title"]},"Roadmap":{"properties":{"base_url":{"type":"string"},"date_format":{"type":"string"},"milestones":{"items":{"$ref":"#/components/schemas/Milestone"},"type":"array"},"prev_id":{"type":"string"},"projects":{"items":{"$ref":"#/components/schemas/Project"},"type":"array"},"title":{"type":"string"}},"required":["title","date_format"]},"RoadmapRequest":{"allOf":[{"$ref":"#/components/schemas/Roadmap"}]},"RoadmapResponse":{"allOf":[{"$ref":"#/components/schemas/Roadmap"}],"properties":{"id":{"type":"string"}},"required":["id"]}}},"info":{"contact":{"email":"[email protected]","name":"Roadmapper Team","url":"https://rdmp.app"},"description":"API for Roadmapper","license":{"name":"ISC","url":"https://opensource.org/licenses/ISC"},"termsOfService":"https://docs.rdmp.app/terms/","title":"Roadmapper API","version":"0.0.1"},"openapi":"3.0.3","paths":{"/":{"post":{"description":"Creates a new roadmap","operationId":"createRoadmap","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoadmapRequest"}}},"description":"Roadmap to create","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoadmapResponse"}}},"description":"Roadmap create success response"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Unexpected error"}}}},"/{roadmap_id}":{"get":{"description":"Retrieves a roadmap","operationId":"getRoadmap","parameters":[{"description":"ID of roadmap to update","in":"path","name":"roadmap_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoadmapResponse"}}},"description":"Roadmap response"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Unexpected error"}}}}},"servers":[{"url":"https://rdmp.app/api"},{"url":"http://localhost:1323/api"}]}
10 changes: 4 additions & 6 deletions api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ info:

servers:
- url: https://rdmp.app/api
- url: http://localhost:1323/api

paths:
/roadmaps:
/:
post:
description: Creates a new roadmap
operationId: createRoadmap
Expand All @@ -29,7 +30,7 @@ paths:
schema:
$ref: '#/components/schemas/RoadmapRequest'
responses:
'204':
'201':
description: Roadmap create success response
content:
application/json:
Expand All @@ -42,7 +43,7 @@ paths:
schema:
$ref: '#/components/schemas/Problem'

/roadmaps/{roadmap_id}:
/{roadmap_id}:
get:
description: Retrieves a roadmap
operationId: getRoadmap
Expand Down Expand Up @@ -86,9 +87,6 @@ components:
required:
- title
- date_format
- base_url
- projects
- milestones
properties:
prev_id:
type: string
Expand Down
266 changes: 266 additions & 0 deletions cmd/roadmapper/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// +build api

package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"time"

gofakeit "github.com/brianvoe/gofakeit/v5"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/peteraba/roadmapper/pkg/roadmap"
)

const (
baseUrl = "http://localhost:1323/api"
yamlFilePath = "../../api.yml"
jsonFilePath = "../../api.json"
)

var (
router *openapi3filter.Router
httpClient *http.Client

minDate = time.Date(2020, 1, 10, 0, 0, 0, 0, time.UTC)
maxDate = time.Date(2020, 4, 25, 0, 0, 0, 0, time.UTC)
)

// init will:
// - provide a seed for all used random generators
// - create a new router from the api.json file
// but first it will check if api.yml is newer than api.json and re-converts it if needed
func init() {
gofakeit.Seed(0)

if httpClient == nil {
httpClient = &http.Client{}
}

if router != nil {
return
}

yamlFile, err := os.Stat(yamlFilePath)
if err != nil {
panic("could not find '" + yamlFilePath + "'" + err.Error())
}

jsonFile, err := os.Stat(jsonFilePath)
if err != nil || yamlFile.ModTime().After(jsonFile.ModTime()) {
content, err := ioutil.ReadFile(yamlFilePath)
if err != nil {
panic("could not read '" + yamlFilePath + "': " + err.Error())
}

jsonContent, err := yaml.YAMLToJSON(content)
if err != nil {
panic("could not parse '" + yamlFilePath + "': " + err.Error())
}

err = ioutil.WriteFile(jsonFilePath, jsonContent, os.ModePerm)
if err != nil {
panic("could not write '" + yamlFilePath + "': " + err.Error())
}

time.Sleep(10 * time.Second)
}

router = openapi3filter.NewRouter().WithSwaggerFromFile(jsonFilePath)
}

// doHttpWithBody sends an HTTP request and returns an HTTP response and the body content
func doHttpWithBody(t *testing.T, req *http.Request, expectedStatusCode int) (*http.Response, []byte) {
resp, err := httpClient.Do(req)
require.NoError(t, err)
require.Equal(t, expectedStatusCode, resp.StatusCode)

body, err := ioutil.ReadAll(resp.Body)

require.NoError(t, err)
resp.Body.Close()
resp.Body = ioutil.NopCloser(bytes.NewBuffer(body))

return resp, body
}

func newRoadmapPayload() roadmap.RoadmapExchange {
p := gofakeit.Number(0, 20)
m := gofakeit.Number(0, p)

var (
milestones []roadmap.Milestone
projects []roadmap.Project
project roadmap.Project
ind = 0
)

for i := 0; i < m; i++ {
milestones = append(milestones, newMilestone())
}

for i := 0; i < p; i++ {
project = newProject(m, ind)
projects = append(projects, project)
ind = nextIndentation(ind)
}

return roadmap.RoadmapExchange{
Title: newWords(),
DateFormat: "2006-01-02",
BaseURL: gofakeit.URL(),
Projects: projects,
Milestones: milestones,
}
}

func newProject(milestoneCount, ind int) roadmap.Project {
m := gofakeit.Number(0, milestoneCount)
d := newDates()
p := gofakeit.Number(0, 100)

project := roadmap.Project{
Indentation: uint8(ind),
Title: newWords(),
Milestone: uint8(m),
Dates: d,
Percentage: uint8(p),
}

return project
}

func newWords() string {
var w []string

for i := 0; i < gofakeit.Number(1, 5); i++ {
w = append(w, gofakeit.HipsterWord())
}

return strings.Join(w, " ")
}

func nextIndentation(indentation int) int {
return indentation - gofakeit.Number(-1, indentation)
}

func newDates() *roadmap.Dates {
if gofakeit.Bool() {
return nil
}

var (
d0 = gofakeit.DateRange(minDate, maxDate)
d1 = gofakeit.DateRange(minDate, maxDate)
)

if d0.Before(d1) {
return &roadmap.Dates{
StartAt: d0,
EndAt: d1,
}
}

return &roadmap.Dates{
StartAt: d1,
EndAt: d0,
}
}

func getURLs() []string {
var (
urls []string
)

for i := 0; i < gofakeit.Number(0, 2); i++ {
urls = append(urls, gofakeit.URL())
}

for i := 0; i < gofakeit.Number(0, 2); i++ {
urls = append(urls, gofakeit.Word())
}

return urls
}

func newMilestone() roadmap.Milestone {
return roadmap.Milestone{
Title: newWords(),
DeadlineAt: newDateOptional(),
URLs: getURLs(),
}
}

func newDateOptional() *time.Time {
var (
optionalDate *time.Time
)

if gofakeit.Bool() {
date := gofakeit.DateRange(minDate, maxDate)
optionalDate = &date
}

return optionalDate
}

func newCreateRoadmapRequest(t *testing.T, re roadmap.RoadmapExchange) *http.Request {
marshaled, err := json.Marshal(re)
require.NoError(t, err)

url := fmt.Sprintf("%s/", baseUrl)
req, err := http.NewRequest("POST", url, bytes.NewReader(marshaled))
require.NoError(t, err)

req.Header.Add("Content-Type", `application/json`)

return req
}

func TestApi_CreateRoadmap(t *testing.T) {
t.Run("success", func(t *testing.T) {
// Create request
roadmapRequestData := newRoadmapPayload()
req := newCreateRoadmapRequest(t, roadmapRequestData)

// Find route in the swagger file
route, pathParams, err := router.FindRoute(req.Method, req.URL)
require.NoError(t, err)

// Validate request
requestValidationInput := &openapi3filter.RequestValidationInput{
Request: req,
PathParams: pathParams,
Route: route,
}
err = openapi3filter.ValidateRequest(nil, requestValidationInput)
assert.NoError(t, err)

// Get response
resp, body := doHttpWithBody(t, req, http.StatusCreated)

// Validate response
err = openapi3filter.ValidateResponse(nil, &openapi3filter.ResponseValidationInput{
RequestValidationInput: requestValidationInput,
Status: resp.StatusCode,
Header: resp.Header,
Body: resp.Body,
})
require.NoError(t, err)

// Read and parse response
response := roadmap.RoadmapExchange{}
err = json.Unmarshal(body, &response)
require.NoError(t, err)
})
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// +build e2e
// +build browser

package main

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.14
require (
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca // indirect
github.com/brianvoe/gofakeit/v5 v5.4.3
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/chromedp/chromedp v0.5.3
github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c // indirect
Expand All @@ -13,6 +14,8 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/getkin/kin-openapi v0.8.0
github.com/ghodss/yaml v1.0.0
github.com/go-pg/pg v8.0.6+incompatible
github.com/go-pg/pg/v9 v9.1.3 // indirect
github.com/gosuri/uitable v0.0.4
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/blend/go-sdk v2.0.0+incompatible/go.mod h1:3GUb0YsHFNTJ6hsJTpzdmCUl05o8HisKjx5OAlzYKdw=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/brianvoe/gofakeit v1.2.0 h1:GGbzCqQx9ync4ObAUhRa3F/M73eL9VZL3X09WoTwphM=
github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8=
github.com/brianvoe/gofakeit/v5 v5.4.3 h1:JWRVZTw81gV1RxNlNmBvZ+1oOqv0U6tMCne3mPXR9N8=
github.com/brianvoe/gofakeit/v5 v5.4.3/go.mod h1:/ZENnKqX+XrN8SORLe/fu5lZDIo1tuPncWuRD+eyhSI=
github.com/cenkalti/backoff v1.1.0 h1:QnvVp8ikKCDWOsFheytRCoYWYPO/ObCTBGxT19Hc+yE=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
Expand Down Expand Up @@ -59,6 +63,10 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE=
github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-pg/pg v8.0.6+incompatible h1:Hi7yUJ2zwmHFq1Mar5XqhCe3NJ7j9r+BaiNmd+vqf+A=
Expand Down

0 comments on commit 9804b62

Please sign in to comment.