Browse Source

A bit of refactoring to add integrations tests

master
Dashie der otter 1 year ago
parent
commit
42cf801980
Signed by: Dashie <dashie@sigpipe.me> GPG Key ID: C2D57B325840B755

+ 27
- 4
.drone.yml View File

@@ -9,6 +9,21 @@ clone:
9 9
     tags: true
10 10
 
11 11
 pipeline:
12
+  linters:
13
+    image: webhippie/golang:edge
14
+    pull: true
15
+    environment:
16
+      TAGS: sqlite
17
+      GOPATH: /srv/app
18
+    commands:
19
+      - apk -U add libmagic file-dev 
20
+      - make clean
21
+      - make vet
22
+      - make lint
23
+      - make misspell-check
24
+    when:
25
+      event: [ push, tag, pull_request ]
26
+
12 27
   test:
13 28
     image: webhippie/golang:edge
14 29
     pull: true
@@ -21,6 +36,18 @@ pipeline:
21 36
     when:
22 37
       event: [ push, tag, pull_request ]
23 38
 
39
+  integration_sqlite:
40
+    image: webhippie/golang:edge
41
+    pull: true
42
+    environment:
43
+      TAGS: sqlite
44
+      GOPATH: /srv/app
45
+    commands:
46
+      - apk -U add libmagic file-dev 
47
+      - make integrations-sqlite
48
+    when:
49
+      event: [ push, tag, pull_request ]
50
+
24 51
   build:
25 52
     image: webhippie/golang:edge
26 53
     pull: true
@@ -29,10 +56,6 @@ pipeline:
29 56
       GOPATH: /srv/app
30 57
     commands:
31 58
       - apk -U add libmagic file-dev 
32
-      - make clean
33
-      - make vet
34
-      - make lint
35
-      - make misspell-check
36 59
       - make build
37 60
     when:
38 61
       event: [ push, tag, pull_request ]

+ 1
- 0
.gitignore View File

@@ -19,3 +19,4 @@ output*
19 19
 myapp
20 20
 myapp
21 21
 myapp.db
22
+integrations.sqlite.test

+ 24
- 10
Makefile View File

@@ -1,5 +1,7 @@
1
-LDFLAGS += -X "dev.sigpipe.me/dashie/myapp/setting.BuildTime=$(shell date -u '+%Y-%m-%d %I:%M:%S %Z')"
2
-LDFLAGS += -X "dev.sigpipe.me/dashie/myapp/setting.BuildGitHash=$(shell git rev-parse HEAD)"
1
+GOPKGNAMEPATH = dev.sigpipe.me/dashie/myapp
2
+
3
+LDFLAGS += -X "$(GOPKGNAMEPATH)/setting.BuildTime=$(shell date -u '+%Y-%m-%d %I:%M:%S %Z')"
4
+LDFLAGS += -X "$(GOPKGNAMEPATH)/setting.BuildGitHash=$(shell git rev-parse HEAD)"
3 5
 
4 6
 OS := $(shell uname)
5 7
 
@@ -21,7 +23,9 @@ GOLINT=golint -set_exit_status
21 23
 GO ?= go
22 24
 
23 25
 GOFILES := $(shell find . -name "*.go" -type f ! -path "./vendor/*" ! -path "*/bindata.go")
24
-PACKAGES ?= $(filter-out dev.sigpipe.me/dashie/myapp/integrations,$(shell go list ./... | grep -v /vendor/))
26
+PACKAGES ?= $(filter-out ${GOPKGNAMEPATH}/integrations,$(shell go list ./... | grep -v /vendor/))
27
+PACKAGES_ALL ?= $(shell go list ./... | grep -v /vendor/)
28
+SOURCES ?= $(shell find . -name "*.go" -type f)
25 29
 XGO_DEPS = ""
26 30
 
27 31
 ifneq ($(DRONE_TAG),)
@@ -34,14 +38,12 @@ else
34 38
 	endif
35 39
 endif
36 40
 
37
-### Targets
41
+### Targets build and checks
38 42
 
39 43
 .PHONY: build clean
40 44
 
41 45
 all: build
42 46
 
43
-check: test
44
-
45 47
 web: build
46 48
 	./$(EXECUTABLE) web
47 49
 
@@ -52,7 +54,7 @@ lint:
52 54
 	@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
53 55
 		$(GO) get -u github.com/golang/lint/golint; \
54 56
 	fi
55
-	for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
57
+	for PKG in $(PACKAGES_ALL); do golint -set_exit_status $$PKG || exit 1; done;
56 58
 
57 59
 build:
58 60
 	$(GO) build $(BUILD_FLAGS) -ldflags '$(LDFLAGS)' -tags '$(TAGS)'
@@ -69,9 +71,6 @@ clean:
69 71
 clean-mac: clean
70 72
 	find . -name ".DS_Store" -delete
71 73
 
72
-test:
73
-	$(GO) test -cover -v $(PACKAGES)
74
-
75 74
 .PHONY: misspell-check
76 75
 misspell-check:
77 76
 	@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
@@ -86,6 +85,21 @@ misspell:
86 85
 	fi
87 86
 	misspell -w -i unknwon $(GOFILES)
88 87
 
88
+### Targets for tests
89
+check: test
90
+
91
+test:
92
+	$(GO) test -cover -v $(PACKAGES)
93
+
94
+integrations.sqlite.test: $(SOURCES)
95
+	$(GO) test -c $(GOPKGNAMEPATH)/integrations -o integrations.sqlite.test -tags 'sqlite'
96
+
97
+.PHONY: integrations-sqlite
98
+integrations-sqlite: integrations.sqlite.test
99
+	APP_ROOT=${CURDIR} APP_CONF=integrations/sqlite.ini ./integrations.sqlite.test
100
+
101
+### Targets for releases
102
+
89 103
 .PHONY: release
90 104
 release: release-dirs release-windows release-linux release-copy release-check
91 105
 

+ 20
- 67
cmd/web.go View File

@@ -4,15 +4,11 @@ import (
4 4
 	"dev.sigpipe.me/dashie/myapp/context"
5 5
 	"dev.sigpipe.me/dashie/myapp/models"
6 6
 	"dev.sigpipe.me/dashie/myapp/routers"
7
-	"dev.sigpipe.me/dashie/myapp/routers/admin"
8
-	"dev.sigpipe.me/dashie/myapp/routers/user"
9 7
 	"dev.sigpipe.me/dashie/myapp/setting"
10 8
 	"dev.sigpipe.me/dashie/myapp/stuff/cron"
11
-	"dev.sigpipe.me/dashie/myapp/stuff/form"
12 9
 	"dev.sigpipe.me/dashie/myapp/stuff/mailer"
13 10
 	"dev.sigpipe.me/dashie/myapp/stuff/template"
14 11
 	"fmt"
15
-	"github.com/go-macaron/binding"
16 12
 	"github.com/go-macaron/cache"
17 13
 	"github.com/go-macaron/csrf"
18 14
 	"github.com/go-macaron/i18n"
@@ -41,7 +37,8 @@ var Web = cli.Command{
41 37
 	},
42 38
 }
43 39
 
44
-func newMacaron() *macaron.Macaron {
40
+// NewMacaron initialize a new macaron
41
+func NewMacaron() *macaron.Macaron {
45 42
 	m := macaron.New()
46 43
 	if !setting.DisableRouterLog {
47 44
 		m.Use(macaron.Logger())
@@ -108,78 +105,34 @@ func newMacaron() *macaron.Macaron {
108 105
 
109 106
 }
110 107
 
111
-func runWeb(ctx *cli.Context) error {
112
-	if ctx.IsSet("config") {
113
-		setting.CustomConf = ctx.String("config")
108
+func checkRunMode() {
109
+	if setting.ProdMode {
110
+		macaron.Env = macaron.PROD
111
+		macaron.ColorLog = false
112
+	} else {
113
+		macaron.Env = macaron.DEV
114 114
 	}
115
+	log.Info("Run Mode: %s", strings.Title(macaron.Env))
116
+}
115 117
 
118
+// GlobalInit the macaron
119
+func GlobalInit() {
116 120
 	setting.InitConfig()
117 121
 	models.InitDb()
118 122
 	cron.NewContext()
119 123
 	mailer.NewContext()
120 124
 
121
-	m := newMacaron()
125
+	checkRunMode()
126
+}
122 127
 
123
-	if setting.ProdMode {
124
-		macaron.Env = macaron.PROD
125
-		macaron.ColorLog = false
126
-	} else {
127
-		macaron.Env = macaron.DEV
128
+func runWeb(ctx *cli.Context) error {
129
+	if ctx.IsSet("config") {
130
+		setting.CustomConf = ctx.String("config")
128 131
 	}
129
-	log.Info("Run Mode: %s", strings.Title(macaron.Env))
130
-
131
-	reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
132
-	reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
133
-
134
-	bindIgnErr := binding.BindIgnErr
135
-
136
-	m.Get("/", func(ctx *context.Context) {})
137
-
138
-	m.Group("/user", func() {
139
-		m.Group("/login", func() {
140
-			m.Combo("").Get(user.Login).Post(bindIgnErr(form.Login{}), user.LoginPost)
141
-		})
142
-		m.Get("/register", user.Register)
143
-		m.Post("/register", csrf.Validate, bindIgnErr(form.Register{}), user.RegisterPost)
144
-		m.Get("/reset_password", user.ResetPasswd)
145
-		m.Post("/reset_password", user.ResetPasswdPost)
146
-	}, reqSignOut)
147
-
148
-	m.Group("/user/settings", func() {
149
-		m.Get("", user.Settings)
150
-		m.Post("", csrf.Validate, bindIgnErr(form.UpdateSettingsProfile{}), user.SettingsPost)
151
-	}, reqSignIn, func(ctx *context.Context) {
152
-		ctx.Data["PageIsUserSettings"] = true
153
-	})
154
-
155
-	m.Group("/user", func() {
156
-		m.Get("/logout", user.Logout)
157
-	}, reqSignIn)
158
-
159
-	m.Group("/user", func() {
160
-		m.Get("/forget_password", user.ForgotPasswd)
161
-		m.Post("/forget_password", user.ForgotPasswdPost)
162
-	})
163
-
164
-	// END USER
165
-
166
-	/* Admin part */
167
-
168
-	adminReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})
169
-	m.Group("/admin", func() {
170
-		m.Get("", adminReq, admin.Dashboard)
171
-	}, adminReq)
172
-
173
-	// robots.txt
174
-	m.Get("/robots.txt", func(ctx *context.Context) {
175
-		if setting.HasRobotsTxt {
176
-			ctx.ServeFileContent(setting.RobotsTxtPath)
177
-		} else {
178
-			ctx.Error(404)
179
-		}
180
-	})
181 132
 
182
-	m.NotFound(routers.NotFound)
133
+	GlobalInit()
134
+	m := NewMacaron()
135
+	routers.RegisterRoutes(m)
183 136
 
184 137
 	if ctx.IsSet("port") {
185 138
 		setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, ctx.String("port"), 1)

+ 1
- 0
conf/app.ini View File

@@ -26,6 +26,7 @@ PASSWD =
26 26
 SSL_MODE = disable
27 27
 ; For "sqlite3" and "tidb", use absolute path when you start as service
28 28
 PATH = myapp.db
29
+LOGGING = true
29 30
 
30 31
 [log]
31 32
 ROOT_PATH =

+ 3
- 0
conf/locale/locale_en-US.ini View File

@@ -1,3 +1,6 @@
1
+[app]
2
+home_title = "Homepage"
3
+
1 4
 [login]
2 5
 title = "Sign In"
3 6
 sign_in = "Sign In"

+ 42
- 0
integrations/html_helper.go View File

@@ -0,0 +1,42 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package integrations
6
+
7
+import (
8
+	"bytes"
9
+	"testing"
10
+
11
+	"github.com/PuerkitoBio/goquery"
12
+	"github.com/stretchr/testify/assert"
13
+)
14
+
15
+// HTMLDoc struct
16
+type HTMLDoc struct {
17
+	doc *goquery.Document
18
+}
19
+
20
+// NewHTMLParser parse html file
21
+func NewHTMLParser(t testing.TB, content []byte) *HTMLDoc {
22
+	doc, err := goquery.NewDocumentFromReader(bytes.NewReader(content))
23
+	assert.NoError(t, err)
24
+	return &HTMLDoc{doc: doc}
25
+}
26
+
27
+// GetInputValueByID for get input value by id
28
+func (doc *HTMLDoc) GetInputValueByID(id string) string {
29
+	text, _ := doc.doc.Find("#" + id).Attr("value")
30
+	return text
31
+}
32
+
33
+// GetInputValueByName for get input value by name
34
+func (doc *HTMLDoc) GetInputValueByName(name string) string {
35
+	text, _ := doc.doc.Find("input[name=\"" + name + "\"]").Attr("value")
36
+	return text
37
+}
38
+
39
+// GetCSRF for get CSRC token value from input
40
+func (doc *HTMLDoc) GetCSRF() string {
41
+	return doc.GetInputValueByName("_csrf")
42
+}

+ 36
- 0
integrations/hub_test.go View File

@@ -0,0 +1,36 @@
1
+package integrations
2
+
3
+import (
4
+	"dev.sigpipe.me/dashie/myapp/setting"
5
+	"fmt"
6
+	"github.com/Unknwon/i18n"
7
+	. "github.com/smartystreets/goconvey/convey"
8
+	"net/http"
9
+	"testing"
10
+)
11
+
12
+func TestHome(t *testing.T) {
13
+	Convey("Test Home page", t, func() {
14
+		prepareTestEnv(t)
15
+
16
+		req := NewRequest(t, "GET", "/")
17
+		resp := MakeRequest(t, req, http.StatusOK)
18
+
19
+		title := fmt.Sprintf("<title>%s - %s</title>", i18n.Tr("en", "app.home_title"), setting.AppName)
20
+
21
+		So(string(resp.Body), ShouldContainSubstring, title)
22
+	})
23
+}
24
+
25
+// TODO: reimplement the impressum
26
+//func TestImpressum(t *testing.T) {
27
+//	Convey("Test Impressum not enabled", t, func() {
28
+//		// disabled by default
29
+//		prepareTestEnv(t)
30
+//
31
+//		req := NewRequest(t, "GET", "/impressum")
32
+//		resp := MakeRequest(t, req, http.StatusNotFound)
33
+//
34
+//		So(resp.HeaderCode, ShouldEqual, 404)
35
+//	})
36
+//}

+ 256
- 0
integrations/integration_test.go View File

@@ -0,0 +1,256 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+// /!\ Warning /!\
6
+// While the rest of the app does use URLFor from Macaron Context
7
+// We don't use it in the integration test
8
+// Maybe we can manage to add it but for now, reflect any URL change to the integrations
9
+// tips: use some syntax like in a comment: "// URLName: XXXHERE" if the route is defined with ".Name("XXXHERE")"
10
+//       to search easily a route to change or whatever.
11
+// /!\ Warning /!\
12
+
13
+package integrations
14
+
15
+import (
16
+	"bytes"
17
+	"dev.sigpipe.me/dashie/myapp/cmd"
18
+	"dev.sigpipe.me/dashie/myapp/models"
19
+	"dev.sigpipe.me/dashie/myapp/routers"
20
+	"dev.sigpipe.me/dashie/myapp/setting"
21
+	"encoding/json"
22
+	"fmt"
23
+	"github.com/go-testfixtures/testfixtures"
24
+	"github.com/stretchr/testify/assert"
25
+	"gopkg.in/macaron.v1"
26
+	"io"
27
+	"net/http"
28
+	"net/http/cookiejar"
29
+	"net/url"
30
+	"os"
31
+	"path"
32
+	"strings"
33
+	"testing"
34
+)
35
+
36
+var mac *macaron.Macaron
37
+
38
+func TestMain(m *testing.M) {
39
+	initIntegrationTest()
40
+	mac = cmd.NewMacaron()
41
+	routers.RegisterRoutes(mac)
42
+
43
+	var helper testfixtures.Helper
44
+	if setting.UseMySQL {
45
+		helper = &testfixtures.MySQL{}
46
+	} else if setting.UsePostgreSQL {
47
+		helper = &testfixtures.PostgreSQL{}
48
+	} else if setting.UseSQLite3 {
49
+		helper = &testfixtures.SQLite{}
50
+	} else {
51
+		fmt.Println("Unsupported RDBMS for integration tests")
52
+		os.Exit(1)
53
+	}
54
+
55
+	err := models.InitFixtures(helper, "models/fixtures/")
56
+	if err != nil {
57
+		fmt.Printf("Error initializing test database: %v\n", err)
58
+		os.Exit(1)
59
+	}
60
+	os.Exit(m.Run())
61
+}
62
+
63
+func initIntegrationTest() {
64
+	appRoot := os.Getenv("APP_ROOT")
65
+	if appRoot == "" {
66
+		fmt.Println("Environment variable $APP_ROOT is not set")
67
+		os.Exit(1)
68
+	}
69
+	setting.AppPath = path.Join(appRoot, "myapp")
70
+
71
+	appConf := os.Getenv("APP_CONF")
72
+	if appConf == "" {
73
+		fmt.Println("Environment variable $APP_CONF is not set")
74
+		os.Exit(1)
75
+	} else if !path.IsAbs(appConf) {
76
+		setting.CustomConf = path.Join(appRoot, appConf)
77
+	} else {
78
+		setting.CustomConf = appConf
79
+	}
80
+
81
+	setting.InitConfig()
82
+	models.InitDb()
83
+	cmd.GlobalInit()
84
+}
85
+
86
+func prepareTestEnv(t testing.TB) {
87
+	assert.NoError(t, models.LoadFixtures())
88
+	//assert.NoError(t, os.RemoveAll("integrations/myapp-integration"))
89
+	//assert.NoError(t, com.CopyDir("integrations/myapp-integration-meta", "integrations/myapp-integration"))
90
+}
91
+
92
+type TestSession struct {
93
+	jar http.CookieJar
94
+}
95
+
96
+func (s *TestSession) GetCookie(name string) *http.Cookie {
97
+	baseURL, err := url.Parse(setting.AppURL)
98
+	if err != nil {
99
+		return nil
100
+	}
101
+
102
+	for _, c := range s.jar.Cookies(baseURL) {
103
+		if c.Name == name {
104
+			return c
105
+		}
106
+	}
107
+	return nil
108
+}
109
+
110
+func (s *TestSession) MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *TestResponse {
111
+	baseURL, err := url.Parse(setting.AppURL)
112
+	assert.NoError(t, err)
113
+	for _, c := range s.jar.Cookies(baseURL) {
114
+		req.AddCookie(c)
115
+	}
116
+	resp := MakeRequest(t, req, expectedStatus)
117
+
118
+	ch := http.Header{}
119
+	ch.Add("Cookie", strings.Join(resp.Headers["Set-Cookie"], ";"))
120
+	cr := http.Request{Header: ch}
121
+	s.jar.SetCookies(baseURL, cr.Cookies())
122
+
123
+	return resp
124
+}
125
+
126
+const userPassword = "password"
127
+
128
+var loginSessionCache = make(map[string]*TestSession, 10)
129
+
130
+func loginUser(t testing.TB, userName string) *TestSession {
131
+	if session, ok := loginSessionCache[userName]; ok {
132
+		return session
133
+	}
134
+	session := loginUserWithPassword(t, userName, userPassword)
135
+	loginSessionCache[userName] = session
136
+	return session
137
+}
138
+
139
+func loginUserWithPassword(t testing.TB, userName, password string) *TestSession {
140
+	req := NewRequest(t, "GET", "/user/login")
141
+	resp := MakeRequest(t, req, http.StatusOK)
142
+
143
+	doc := NewHTMLParser(t, resp.Body)
144
+	req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
145
+		"_csrf":     doc.GetCSRF(),
146
+		"user_name": userName,
147
+		"password":  password,
148
+	})
149
+	resp = MakeRequest(t, req, http.StatusFound)
150
+
151
+	ch := http.Header{}
152
+	ch.Add("Cookie", strings.Join(resp.Headers["Set-Cookie"], ";"))
153
+	cr := http.Request{Header: ch}
154
+
155
+	jar, err := cookiejar.New(nil)
156
+	assert.NoError(t, err)
157
+	baseURL, err := url.Parse(setting.AppURL)
158
+	assert.NoError(t, err)
159
+	jar.SetCookies(baseURL, cr.Cookies())
160
+
161
+	return &TestSession{jar: jar}
162
+}
163
+
164
+type TestResponseWriter struct {
165
+	HeaderCode int
166
+	Writer     io.Writer
167
+	Headers    http.Header
168
+}
169
+
170
+func (w *TestResponseWriter) Header() http.Header {
171
+	return w.Headers
172
+}
173
+
174
+func (w *TestResponseWriter) Write(b []byte) (int, error) {
175
+	return w.Writer.Write(b)
176
+}
177
+
178
+func (w *TestResponseWriter) WriteHeader(n int) {
179
+	w.HeaderCode = n
180
+}
181
+
182
+type TestResponse struct {
183
+	HeaderCode int
184
+	Body       []byte
185
+	Headers    http.Header
186
+}
187
+
188
+func NewRequest(t testing.TB, method, urlStr string) *http.Request {
189
+	return NewRequestWithBody(t, method, urlStr, nil)
190
+}
191
+
192
+func NewRequestf(t testing.TB, method, urlFormat string, args ...interface{}) *http.Request {
193
+	return NewRequest(t, method, fmt.Sprintf(urlFormat, args...))
194
+}
195
+
196
+func NewRequestWithValues(t testing.TB, method, urlStr string, values map[string]string) *http.Request {
197
+	urlValues := url.Values{}
198
+	for key, value := range values {
199
+		urlValues[key] = []string{value}
200
+	}
201
+	req := NewRequestWithBody(t, method, urlStr, bytes.NewBufferString(urlValues.Encode()))
202
+	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
203
+	return req
204
+}
205
+
206
+func NewRequestWithJSON(t testing.TB, method, urlStr string, v interface{}) *http.Request {
207
+	jsonBytes, err := json.Marshal(v)
208
+	assert.NoError(t, err)
209
+	req := NewRequestWithBody(t, method, urlStr, bytes.NewBuffer(jsonBytes))
210
+	req.Header.Add("Content-Type", "application/json")
211
+	return req
212
+}
213
+
214
+func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *http.Request {
215
+	request, err := http.NewRequest(method, urlStr, body)
216
+	assert.NoError(t, err)
217
+	request.RequestURI = urlStr
218
+	return request
219
+}
220
+
221
+const NoExpectedStatus = -1
222
+
223
+func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *TestResponse {
224
+	buffer := bytes.NewBuffer(nil)
225
+	respWriter := &TestResponseWriter{
226
+		Writer:  buffer,
227
+		Headers: make(map[string][]string),
228
+	}
229
+	mac.ServeHTTP(respWriter, req)
230
+	if expectedStatus != NoExpectedStatus {
231
+		assert.EqualValues(t, expectedStatus, respWriter.HeaderCode)
232
+	}
233
+	return &TestResponse{
234
+		HeaderCode: respWriter.HeaderCode,
235
+		Body:       buffer.Bytes(),
236
+		Headers:    respWriter.Headers,
237
+	}
238
+}
239
+
240
+func DecodeJSON(t testing.TB, resp *TestResponse, v interface{}) {
241
+	decoder := json.NewDecoder(bytes.NewBuffer(resp.Body))
242
+	assert.NoError(t, decoder.Decode(v))
243
+}
244
+
245
+func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
246
+	req := NewRequest(t, "GET", urlStr)
247
+	resp := session.MakeRequest(t, req, http.StatusOK)
248
+	doc := NewHTMLParser(t, resp.Body)
249
+	return doc.GetCSRF()
250
+}
251
+
252
+func RedirectURL(t testing.TB, resp *TestResponse) string {
253
+	urlSlice := resp.Headers["Location"]
254
+	assert.NotEmpty(t, urlSlice, "No redirect URL founds")
255
+	return urlSlice[0]
256
+}

+ 192
- 0
integrations/sqlite.ini View File

@@ -0,0 +1,192 @@
1
+APP_NAME = myapp
2
+CAN_REGISTER = true
3
+; Either "dev", "prod" or "test"
4
+RUN_MODE = prod
5
+; Do your laws forces you to have an Impressum-like ?
6
+; You can true this variable and then edit "templates/impressum.tmpl"
7
+; The link will be added automatically in the footer and the page handled too
8
+NEEDS_IMPRESSUM = false
9
+
10
+[server]
11
+PROTOCOL = http
12
+DOMAIN = localhost
13
+ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
14
+UNIX_SOCKET_PERMISSION = 666
15
+HTTP_ADDR = 0.0.0.0
16
+HTTP_PORT = 4000
17
+DISABLE_ROUTER_LOG = false
18
+; Upper level of template and static file path
19
+; default is the path where myapp is executed
20
+STATIC_ROOT_PATH =
21
+
22
+[database]
23
+; Either "mysql", "postgres" or "sqlite3", you can connect to TiDB with MySQL protocol
24
+DB_TYPE = sqlite3
25
+HOST = 127.0.0.1:3306
26
+NAME = myapp
27
+USER = root
28
+PASSWD =
29
+; For "postgres" only, either "disable", "require" or "verify-full"
30
+SSL_MODE = disable
31
+; For "sqlite3" and "tidb", use absolute path when you start as service
32
+PATH = :memory:
33
+#PATH = test-integrations.sqlite3
34
+LOGGING = false
35
+
36
+[log]
37
+ROOT_PATH =
38
+
39
+; Can be "console" and "file", default is "console"
40
+; Use comma to separate multiple modes, e.g. "console, file"
41
+MODE = console
42
+; Buffer length of channel, keep it as it is if you don't know what it is.
43
+BUFFER_LEN = 100
44
+; Either "Trace", "Info", "Warn", "Error", "Fatal", default is "Trace"
45
+LEVEL = Trace
46
+
47
+; For "console" mode only
48
+[log.console]
49
+; leave empty to inherit
50
+LEVEL =
51
+
52
+; For "file" mode only
53
+[log.file]
54
+; leave empty to inherit
55
+LEVEL =
56
+; This enables automated log rotate (switch of following options)
57
+LOG_ROTATE = true
58
+; Segment log daily
59
+DAILY_ROTATE = true
60
+; Max size shift of single file, default is 28 means 1 << 28, 256MB
61
+MAX_SIZE_SHIFT = 28
62
+; Max line number of single file
63
+MAX_LINES = 1000000
64
+; Expired days of log file (delete after max days)
65
+MAX_DAYS = 7
66
+
67
+; For "slack" mode only
68
+[log.slack]
69
+; leave empty to inherit
70
+LEVEL =
71
+; Webhook URL
72
+URL =
73
+
74
+[log.gorm]
75
+; Enable file rotation
76
+ROTATE = true
77
+; Rotate every day
78
+ROTATE_DAILY = true
79
+; Rotate once file size excesses x MB
80
+MAX_SIZE = 100
81
+; Maximum days to keep logger files
82
+MAX_DAYS = 3
83
+
84
+[session]
85
+; Either "memory", "file", or "redis", default is "memory"
86
+PROVIDER = file
87
+; Provider config options
88
+; memory: not have any config yet
89
+; file: session file path, e.g. `data/sessions`
90
+; redis: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180
91
+; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
92
+PROVIDER_CONFIG = data/sessions
93
+; Session cookie name
94
+COOKIE_NAME = i_like_ponies
95
+; If you use session in https only, default is false
96
+COOKIE_SECURE = false
97
+; Enable set cookie, default is true
98
+ENABLE_SET_COOKIE = true
99
+; Session GC time interval, default is 3600
100
+GC_INTERVAL_TIME = 3600
101
+; Session life time, default is 86400
102
+SESSION_LIFE_TIME = 86400
103
+; Cookie name for CSRF
104
+CSRF_COOKIE_NAME = _csrf
105
+
106
+[security]
107
+; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!!
108
+SECRET_KEY = !#@FDEWREWR&*(
109
+INSTALL_LOCK = false
110
+; Auto-login remember days
111
+LOGIN_REMEMBER_DAYS = 7
112
+COOKIE_USERNAME = myapp_celestia
113
+COOKIE_REMEMBER_NAME = myapp_luna
114
+COOKIE_SECURE = false
115
+; Enable to set cookie to indicate user login status
116
+ENABLE_LOGIN_STATUS_COOKIE = false
117
+LOGIN_STATUS_COOKIE_NAME = login_status
118
+
119
+[i18n]
120
+LANGS = en-US
121
+NAMES = English
122
+
123
+[cron]
124
+; Enable running cron tasks periodically.
125
+ENABLED = true
126
+; Run cron tasks when myapp starts.
127
+RUN_AT_START = false
128
+
129
+; Cleanup repository archives
130
+[cron.repo_archive_cleanup]
131
+RUN_AT_START = true
132
+SCHEDULE = @every 24h
133
+; Time duration to check if archive should be cleaned
134
+OLDER_THAN = 24h
135
+
136
+[markdown]
137
+; Enable hard line break extension
138
+ENABLE_HARD_LINE_BREAK = false
139
+; List of custom URL-Schemes that are allowed as links when rendering Markdown
140
+; for example git,magnet
141
+CUSTOM_URL_SCHEMES =
142
+; List of file extensions that should be rendered/edited as Markdown
143
+; Separate extensions with a comma. To render files w/o extension as markdown, just put a comma
144
+FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd
145
+
146
+[smartypants]
147
+ENABLED = false
148
+FRACTIONS = true
149
+DASHES = true
150
+LATEX_DASHES = true
151
+ANGLED_QUOTES = true
152
+
153
+[mailer]
154
+ENABLED = true
155
+; Buffer length of channel, keep it as it is if you don't know what it is.
156
+SEND_BUFFER_LEN = 100
157
+; Name displayed in mail title
158
+SUBJECT = %(APP_NAME)s
159
+; Mail server
160
+; Gmail: smtp.gmail.com:587
161
+; QQ: smtp.qq.com:465
162
+; Note, if the port ends with "465", SMTPS will be used. Using STARTTLS on port 587 is recommended per RFC 6409. If the server supports STARTTLS it will always be used.
163
+HOST =
164
+; Disable HELO operation when hostname are different.
165
+DISABLE_HELO =
166
+; Custom hostname for HELO operation, default is from system.
167
+HELO_HOSTNAME =
168
+; Do not verify the certificate of the server. Only use this for self-signed certificates
169
+SKIP_VERIFY =
170
+; Use client certificate
171
+USE_CERTIFICATE = false
172
+CERT_FILE = conf/mailer/cert.pem
173
+KEY_FILE = conf/mailer/key.pem
174
+; Mail from address, RFC 5322. This can be just an email address, or the `"Name" <email@example.com>` format
175
+FROM =
176
+; Mailer user name and password
177
+USER =
178
+PASSWD =
179
+; Use text/plain as format of content
180
+USE_PLAIN_TEXT = false
181
+
182
+[worker]
183
+REDIS_HOST = 127.0.0.1
184
+REDIS_PORT = 6379
185
+REDIS_DB = 1
186
+
187
+[storage]
188
+; Default is binary "Current PWD"/uploads
189
+PATH =
190
+
191
+[audio]
192
+AUDIOWAVEFORM_BIN = /usr/local/bin/audiowaveform

+ 77
- 0
integrations/user_test.go View File

@@ -0,0 +1,77 @@
1
+package integrations
2
+
3
+import (
4
+	"dev.sigpipe.me/dashie/myapp/setting"
5
+	"fmt"
6
+	"github.com/Unknwon/i18n"
7
+	. "github.com/smartystreets/goconvey/convey"
8
+	"net/http"
9
+	"testing"
10
+)
11
+
12
+func TestLogin(t *testing.T) {
13
+	Convey("Test Login User A and access to settings page", t, func() {
14
+		prepareTestEnv(t)
15
+
16
+		session := loginUser(t, "userA")
17
+
18
+		// then test settings page
19
+		req := NewRequest(t, "GET", "/user/settings")
20
+		resp := session.MakeRequest(t, req, http.StatusOK)
21
+
22
+		title := fmt.Sprintf("<title>%s - %s</title>", i18n.Tr("en", "settings.title"), setting.AppName)
23
+
24
+		So(string(resp.Body), ShouldContainSubstring, title)
25
+		So(string(resp.Body), ShouldContainSubstring, "usera@example.com")
26
+	})
27
+}
28
+
29
+func TestLogout(t *testing.T) {
30
+	Convey("Test Login User A and then logout", t, func() {
31
+		prepareTestEnv(t)
32
+
33
+		session := loginUser(t, "userA")
34
+
35
+		req := NewRequest(t, "GET", "/user/logout")
36
+		resp := session.MakeRequest(t, req, http.StatusFound)
37
+
38
+		var redirectTo = "<a href=\"/\">Found</a>"
39
+
40
+		So(resp.HeaderCode, ShouldEqual, 302)
41
+		So(string(resp.Body), ShouldContainSubstring, redirectTo)
42
+	})
43
+}
44
+
45
+func TestRegister(t *testing.T) {
46
+	Convey("Test register user C", t, func() {
47
+		prepareTestEnv(t)
48
+
49
+		req := NewRequest(t, "GET", "/user/register")
50
+		resp := MakeRequest(t, req, http.StatusOK)
51
+
52
+		// For CSRF
53
+		doc := NewHTMLParser(t, resp.Body)
54
+
55
+		// Register
56
+		req = NewRequestWithValues(t, "POST", "/user/register", map[string]string{
57
+			"_csrf":     doc.GetCSRF(),
58
+			"user_name": "userC",
59
+			"email":     "userc@example.com",
60
+			"password":  "password",
61
+			"repeat":    "password",
62
+		})
63
+		MakeRequest(t, req, http.StatusFound)
64
+
65
+		// Login
66
+		session := loginUser(t, "userC")
67
+
68
+		// then test settings page
69
+		req = NewRequest(t, "GET", "/user/settings")
70
+		resp = session.MakeRequest(t, req, http.StatusOK)
71
+
72
+		title := fmt.Sprintf("<title>%s - %s</title>", i18n.Tr("en", "settings.title"), setting.AppName)
73
+
74
+		So(string(resp.Body), ShouldContainSubstring, title)
75
+		So(string(resp.Body), ShouldContainSubstring, "userc@example.com")
76
+	})
77
+}

+ 21
- 0
models/fixtures/users.yml View File

@@ -0,0 +1,21 @@
1
+# NOTE: all users should have a password of "password"
2
+
3
+- # NOTE: this user (id=1) is the admin
4
+  id: 1
5
+  user_name: userA
6
+  lower_name: usera
7
+  email: usera@example.com
8
+  password: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
9
+  salt: ZogKvWdyEx
10
+  is_admin: true
11
+  is_active: true
12
+
13
+-
14
+  id: 2
15
+  user_name: userB
16
+  lower_name: userb
17
+  email: userb@example.com
18
+  password: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
19
+  salt: ZogKvWdyEx
20
+  is_admin: false
21
+  is_active: true

+ 3
- 1
models/models.go View File

@@ -27,6 +27,7 @@ var (
27 27
 
28 28
 	DbCfg struct {
29 29
 		Type, Host, Name, User, Passwd, Path, SSLMode string
30
+		Logging                                       bool
30 31
 	}
31 32
 
32 33
 	EnableSQLite3 bool
@@ -54,6 +55,7 @@ func LoadConfigs() {
54 55
 	}
55 56
 	DbCfg.SSLMode = sec.Key("SSL_MODE").String()
56 57
 	DbCfg.Path = sec.Key("PATH").MustString("myapp.db")
58
+	DbCfg.Logging = sec.Key("LOGGING").MustBool(true)
57 59
 }
58 60
 
59 61
 // parsePostgreSQLHostPort parses given input in various forms defined in
@@ -164,7 +166,7 @@ func SetEngine() (err error) {
164 166
 	//	return fmt.Errorf("fail to create 'gorm.log': %v", err)
165 167
 	//}
166 168
 
167
-	db.LogMode(true)
169
+	db.LogMode(DbCfg.Logging)
168 170
 	// TODO logger with clog
169 171
 	//db.SetLogger(log.New(os.Stdout, "\r\n", 0))
170 172
 	return nil

+ 20
- 0
models/test_fixtures.go View File

@@ -0,0 +1,20 @@
1
+package models
2
+
3
+import (
4
+	"github.com/go-testfixtures/testfixtures"
5
+)
6
+
7
+var fixtures *testfixtures.Context
8
+
9
+// InitFixtures initialize test fixtures for a test database
10
+func InitFixtures(helper testfixtures.Helper, dir string) (err error) {
11
+	testfixtures.SkipDatabaseNameCheck(true)
12
+	fixtures, err = testfixtures.NewFolder(db.DB(), helper, dir)
13
+	return err
14
+}
15
+
16
+// LoadFixtures load fixtures for a test database
17
+func LoadFixtures() error {
18
+	db.AutoMigrate(&User{})
19
+	return fixtures.Load()
20
+}

+ 71
- 0
routers/routes.go View File

@@ -0,0 +1,71 @@
1
+package routers
2
+
3
+import (
4
+	"dev.sigpipe.me/dashie/myapp/context"
5
+	"dev.sigpipe.me/dashie/myapp/routers/admin"
6
+	"dev.sigpipe.me/dashie/myapp/routers/user"
7
+	"dev.sigpipe.me/dashie/myapp/setting"
8
+	"dev.sigpipe.me/dashie/myapp/stuff/form"
9
+	"github.com/go-macaron/binding"
10
+	"github.com/go-macaron/csrf"
11
+	"gopkg.in/macaron.v1"
12
+)
13
+
14
+// RegisterRoutes as named
15
+func RegisterRoutes(m *macaron.Macaron) {
16
+	reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
17
+	reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
18
+
19
+	bindIgnErr := binding.BindIgnErr
20
+
21
+	m.Get("/", func(ctx *context.Context) {
22
+		ctx.Title("app.home_title")
23
+		ctx.HTML(200, "base/home")
24
+	})
25
+
26
+	m.Group("/user", func() {
27
+		m.Group("/login", func() {
28
+			m.Combo("").Get(user.Login).Post(bindIgnErr(form.Login{}), user.LoginPost)
29
+		})
30
+		m.Get("/register", user.Register)
31
+		m.Post("/register", csrf.Validate, bindIgnErr(form.Register{}), user.RegisterPost)
32
+		m.Get("/reset_password", user.ResetPasswd)
33
+		m.Post("/reset_password", user.ResetPasswdPost)
34
+	}, reqSignOut)
35
+
36
+	m.Group("/user/settings", func() {
37
+		m.Get("", user.Settings)
38
+		m.Post("", csrf.Validate, bindIgnErr(form.UpdateSettingsProfile{}), user.SettingsPost)
39
+	}, reqSignIn, func(ctx *context.Context) {
40
+		ctx.Data["PageIsUserSettings"] = true
41
+	})
42
+
43
+	m.Group("/user", func() {
44
+		m.Get("/logout", user.Logout)
45
+	}, reqSignIn)
46
+
47
+	m.Group("/user", func() {
48
+		m.Get("/forget_password", user.ForgotPasswd)
49
+		m.Post("/forget_password", user.ForgotPasswdPost)
50
+	})
51
+
52
+	// END USER
53
+
54
+	/* Admin part */
55
+
56
+	adminReq := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})
57
+	m.Group("/admin", func() {
58
+		m.Get("", adminReq, admin.Dashboard)
59
+	}, adminReq)
60
+
61
+	// robots.txt
62
+	m.Get("/robots.txt", func(ctx *context.Context) {
63
+		if setting.HasRobotsTxt {
64
+			ctx.ServeFileContent(setting.RobotsTxtPath)
65
+		} else {
66
+			ctx.Error(404)
67
+		}
68
+	})
69
+
70
+	m.NotFound(NotFound)
71
+}

+ 9
- 0
templates/base/home.tmpl View File

@@ -0,0 +1,9 @@
1
+{{template "base/head" .}}
2
+
3
+<div class="row">
4
+    <div class="col-lg-2">
5
+        Home Page
6
+    </div>
7
+</div>
8
+
9
+{{template "base/footer" .}}

Loading…
Cancel
Save