Name: {{ .user.Name }}
5 | 6 |├── secrets
└── .keep
├── db
├── backup
│ └── .keep
└── migrate
│ ├── .keep
│ └── Create-Tables.sql.tmpl
├── src
├── users
│ ├── assets
│ │ ├── scripts
│ │ │ └── users.js
│ │ ├── styles
│ │ │ └── users.css
│ │ └── images
│ │ │ └── .keep
│ ├── views
│ │ ├── create.html.got
│ │ ├── update.html.got
│ │ ├── show.html.got
│ │ ├── password_sent.html.got
│ │ ├── password_reset_mail.html.got
│ │ ├── login.html.got
│ │ ├── row.html.got
│ │ ├── password_reset.html.got
│ │ ├── form.html.got
│ │ └── index.html.got
│ ├── actions
│ │ ├── logout.go
│ │ ├── destroy.go
│ │ ├── show.go
│ │ ├── index.go
│ │ ├── create.go
│ │ ├── update.go
│ │ ├── login.go
│ │ ├── password.go
│ │ └── actions_test.go
│ ├── query.go
│ ├── role.go
│ ├── users_test.go
│ └── users.go
├── app
│ ├── views
│ │ ├── includes.html.got
│ │ ├── home.html.got
│ │ ├── meta.html.got
│ │ ├── admin.html.got
│ │ ├── footer.html.got
│ │ ├── error.html.got
│ │ ├── header.html.got
│ │ └── layout.html.got
│ ├── home.go
│ ├── auth.go
│ ├── routes.go
│ ├── assets
│ │ ├── styles
│ │ │ ├── admin.css
│ │ │ └── app.css
│ │ └── scripts
│ │ │ ├── app.js
│ │ │ └── adom.js
│ ├── app_test.go
│ ├── handlers.go
│ ├── setup.go
│ └── bootstrap.go
└── lib
│ ├── templates
│ └── fragmenta_resources
│ │ ├── assets
│ │ ├── scripts
│ │ │ └── fragmenta_resources.js
│ │ ├── images
│ │ │ └── .keep
│ │ └── styles
│ │ │ └── fragmenta_resources.css
│ │ ├── views
│ │ ├── create.html.got.tmpl
│ │ ├── update.html.got.tmpl
│ │ ├── show.html.got.tmpl
│ │ ├── form.html.got.tmpl
│ │ ├── row.html.got.tmpl
│ │ └── index.html.got.tmpl
│ │ ├── query.go.tmpl
│ │ ├── actions
│ │ ├── show.go.tmpl
│ │ ├── destroy.go.tmpl
│ │ ├── index.go.tmpl
│ │ ├── create.go.tmpl
│ │ ├── update.go.tmpl
│ │ └── actions_test.go.tmpl
│ │ ├── fragmenta_resources.go.tmpl
│ │ └── fragmenta_resources_test.go.tmpl
│ ├── mail
│ ├── views
│ │ ├── template.html.got
│ │ └── layout.html.got
│ ├── email.go
│ ├── mail.go
│ └── adapters
│ │ └── sendgrid
│ │ └── sendgrid.go
│ ├── editable
│ ├── views
│ │ ├── example.html.got
│ │ ├── editable-toolbar-basic.html.got
│ │ ├── editable-toolbar.html.got
│ │ └── editable-toolbar-full.html.got
│ └── assets
│ │ ├── styles
│ │ └── editable.css
│ │ └── scripts
│ │ └── editable.js
│ ├── status
│ ├── status_test.go
│ └── status.go
│ ├── resource
│ ├── urls.go
│ ├── query.go
│ ├── validate.go
│ ├── resource.go
│ ├── resource_test.go
│ └── tests.go
│ └── auth
│ ├── authenticate.go
│ └── authenticate_test.go
├── .gitignore
├── public
├── favicon.ico
├── assets
│ ├── images
│ │ └── app
│ │ │ └── logo.png
│ ├── styles
│ │ ├── normalize.min.css
│ │ └── app-58a0afbf47fbb0647dc34dff83732be2509c4c7e.min.css
│ └── scripts
│ │ └── app-d0d2a631d0b5b2e52b6def992031e9fd1309a66d.min.js
└── robots.txt
├── bin
└── deploy
├── server_test.go
├── LICENSE
├── server.go
└── README.md
/secrets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/db/backup/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/db/migrate/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/users/assets/scripts/users.js:
--------------------------------------------------------------------------------
1 | /* JS for users */
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/*
2 | db/backup/*
3 | secrets/*
4 | log/*
5 |
--------------------------------------------------------------------------------
/src/users/assets/styles/users.css:
--------------------------------------------------------------------------------
1 | /* CSS Styles for users */
--------------------------------------------------------------------------------
/src/users/assets/images/.keep:
--------------------------------------------------------------------------------
1 | # this file exists to ensure git doesn't ignore the empty folder
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fragmenta/fragmenta-app/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/app/views/includes.html.got:
--------------------------------------------------------------------------------
1 | {{ style "normalize.min" }}
2 | {{ style "app" }}
3 |
4 | {{ script "app" }}
5 |
--------------------------------------------------------------------------------
/src/lib/templates/fragmenta_resources/assets/scripts/fragmenta_resources.js:
--------------------------------------------------------------------------------
1 | /* JS for [[ .fragmenta_resources ]] */
--------------------------------------------------------------------------------
/src/lib/templates/fragmenta_resources/assets/images/.keep:
--------------------------------------------------------------------------------
1 | # this file exists to ensure git doesn't ignore the empty folder
--------------------------------------------------------------------------------
/src/lib/templates/fragmenta_resources/assets/styles/fragmenta_resources.css:
--------------------------------------------------------------------------------
1 | /* CSS Styles for [[ .fragmenta_resources ]] */
--------------------------------------------------------------------------------
/public/assets/images/app/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fragmenta/fragmenta-app/HEAD/public/assets/images/app/logo.png
--------------------------------------------------------------------------------
/src/app/views/home.html.got:
--------------------------------------------------------------------------------
1 | A bare-bones app template Name: {{ .user.Name }}{{.title}}
3 |
2 |
3 | {{.message}}
4 |
5 | Thanks,
6 |
7 | {{ .from }}
--------------------------------------------------------------------------------
/src/users/views/create.html.got:
--------------------------------------------------------------------------------
1 | Create User
3 | {{ template "users/views/form.html.got" . }}
4 | Update User
3 | {{ template "users/views/form.html.got" . }}
4 | {{ .user.Name }}
3 | Create [[ .Fragmenta_Resource ]]
3 | {{ template "[[ .fragmenta_resources ]]/views/form.html.got" . }}
4 | Update [[ .Fragmenta_Resource ]]
3 | {{ template "[[ .fragmenta_resources ]]/views/form.html.got" . }}
4 |
We've send you a password reset link, please open your email and click the link.
5 |Name: {{ .[[ .fragmenta_resource ]].Name }}
5 | 6 |Hi {{.name}},
2 | 3 |Someone (hopefully you) has requested a password reset for your account. Follow the link below to set a new password: Reset Password
4 | 5 |Any problems with this email? Please contact us to let us know.
6 | -------------------------------------------------------------------------------- /src/app/views/meta.html.got: -------------------------------------------------------------------------------- 1 | 2 |{{.message}}
4 | {{ if .file }} 5 |File:{{.file}}
6 | {{ end }} 7 | {{ if .error }} 8 |
9 | Error:{{.error}}
10 |
11 | {{ end }}
12 | {{ if .current_user.Anon }}
13 |
14 | {{ end }}
15 | Please enter your email below, and we'll send you a password reset link.
5 | 11 |
<\/li>/gi,'<\/li>');
147 |
148 | // Remove comments and other MS cruft
149 | html = html.replace(//gi,'');
150 | html = html.replace(/ class\=\"MsoNormal\"/gi,'');
151 | html = html.replace(/%s
", names[0])
130 | if !strings.Contains(w.Body.String(), pattern) {
131 | t.Fatalf("[[ .fragmenta_resource ]]actions: unexpected response for HandleShow expected:%s got:%s", pattern, w.Body.String())
132 | }
133 | }
134 |
135 | // Test GET /[[ .fragmenta_resources ]]/123/update
136 | func TestShowUpdate[[ .Fragmenta_Resource ]](t *testing.T) {
137 |
138 | // Create request context
139 | w, c := resource.GetRequestContext("/[[ .fragmenta_resources ]]/1/update", "/[[ .fragmenta_resources ]]/{id:[0-9]+}/update", users.MockAdmin())
140 |
141 | // Run the handler
142 | err := HandleUpdateShow(c)
143 |
144 | // Test the error response
145 | if err != nil || w.Code != http.StatusOK {
146 | t.Fatalf("[[ .fragmenta_resource ]]actions: error handling HandleCreateShow %s", err)
147 | }
148 |
149 | // Test the body for a known pattern
150 | pattern := "resource-update-form"
151 | if !strings.Contains(w.Body.String(), pattern) {
152 | t.Fatalf("[[ .fragmenta_resource ]]actions: unexpected response for HandleCreateShow expected:%s got:%s", pattern, w.Body.String())
153 | }
154 |
155 | }
156 |
157 | // Test POST /[[ .fragmenta_resources ]]/123/update
158 | func TestUpdate[[ .Fragmenta_Resource ]](t *testing.T) {
159 | form := url.Values{}
160 | form.Add("name", names[1])
161 | body := strings.NewReader(form.Encode())
162 |
163 | // Create request context
164 | w, c := resource.PostRequestContext("/[[ .fragmenta_resources ]]/1/update", "/[[ .fragmenta_resources ]]/{id:[0-9]+}/update", body, users.MockAdmin())
165 |
166 | // Run the handler to update the [[ .fragmenta_resource ]]
167 | err := HandleUpdate(c)
168 | if err != nil {
169 | t.Fatalf("[[ .fragmenta_resource ]]actions: error handling HandleUpdate[[ .Fragmenta_Resource ]] %s", err)
170 | }
171 |
172 | // Test we get a redirect after update (to the [[ .fragmenta_resource ]] concerned)
173 | if w.Code != http.StatusFound {
174 | t.Fatalf("[[ .fragmenta_resource ]]actions: unexpected response code for HandleUpdate[[ .Fragmenta_Resource ]] expected:%d got:%d", http.StatusFound, w.Code)
175 | }
176 |
177 | // Check the [[ .fragmenta_resource ]] name is in now value names[1]
178 | [[ .fragmenta_resource ]], err := [[ .fragmenta_resources ]].Find(1)
179 | if err != nil {
180 | t.Fatalf("[[ .fragmenta_resource ]]actions: error finding updated [[ .fragmenta_resource ]] %s", err)
181 | }
182 | if [[ .fragmenta_resource ]].ID != 1 || [[ .fragmenta_resource ]].Name != names[1] {
183 | t.Fatalf("[[ .fragmenta_resource ]]actions: error with updated [[ .fragmenta_resource ]] values: %v", [[ .fragmenta_resource ]])
184 | }
185 |
186 | }
187 |
188 | // Test of POST /[[ .fragmenta_resources ]]/123/destroy
189 | func TestDelete[[ .Fragmenta_Resource ]](t *testing.T) {
190 |
191 | body := strings.NewReader(``)
192 |
193 | // Test permissions - anon users can't destroy [[ .fragmenta_resources ]]
194 |
195 | // Create request context
196 | _, c := resource.PostRequestContext("/[[ .fragmenta_resources ]]/2/destroy", "/[[ .fragmenta_resources ]]/{id:[0-9]+}/destroy", body, users.MockAnon())
197 |
198 | // Run the handler to test failure as anon
199 | err := HandleDestroy(c)
200 | if err == nil { // failure expected
201 | t.Fatalf("[[ .fragmenta_resource ]]actions: unexpected response for HandleDestroy as anon, expected failure")
202 | }
203 |
204 | // Now test deleting the [[ .fragmenta_resource ]] created above as admin
205 | // Create request context
206 | w, c := resource.PostRequestContext("/[[ .fragmenta_resources ]]/1/destroy", "/[[ .fragmenta_resources ]]/{id:[0-9]+}/destroy", body, users.MockAdmin())
207 |
208 | // Run the handler
209 | err = HandleDestroy(c)
210 |
211 | // Test the error response is 302 StatusFound
212 | if err != nil {
213 | t.Fatalf("[[ .fragmenta_resource ]]actions: error handling HandleDestroy %s", err)
214 | }
215 |
216 | // Test we get a redirect after delete
217 | if w.Code != http.StatusFound {
218 | t.Fatalf("[[ .fragmenta_resource ]]actions: unexpected response code for HandleDestroy expected:%d got:%d", http.StatusFound, w.Code)
219 | }
220 |
221 | }
222 |
--------------------------------------------------------------------------------
/src/app/bootstrap.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/hex"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "log"
11 | "os"
12 | "os/exec"
13 | "path"
14 | "path/filepath"
15 | "sort"
16 | "strings"
17 | "time"
18 |
19 | "github.com/fragmenta/query"
20 | )
21 |
22 | // TODO: This should probably go into a bootstrap package within fragmenta?
23 |
24 | const (
25 | fragmentaVersion = "1.2"
26 |
27 | permissions = 0744
28 | createDatabaseMigrationName = "Create-Database"
29 | createTablesMigrationName = "Create-Tables"
30 | )
31 |
32 | var (
33 | // ConfigDevelopment holds the development config from fragmenta.json
34 | ConfigDevelopment map[string]string
35 |
36 | // ConfigProduction holds development config from fragmenta.json
37 | ConfigProduction map[string]string
38 |
39 | // ConfigTest holds the app test config from fragmenta.json
40 | ConfigTest map[string]string
41 | )
42 |
43 | // Bootstrap generates missing config files, sql migrations, and runs the first migrations
44 | // For this we need to know what to call the app, but we default to fragmenta-cms for now
45 | // we could use our current folder name?
46 | func Bootstrap() error {
47 | // We assume we're being run from root of project path
48 | projectPath, err := os.Getwd()
49 | if err != nil {
50 | return err
51 | }
52 |
53 | fmt.Printf("\nBootstrapping server...\n")
54 |
55 | err = generateConfig(projectPath)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | err = generateCreateSQL(projectPath)
61 | if err != nil {
62 | return err
63 | }
64 |
65 | // Run the migrations without the fragmenta tool being present
66 | err = runMigrations(projectPath)
67 | if err != nil {
68 | return err
69 | }
70 |
71 | return nil
72 | }
73 |
74 | // RequiresBootStrap returns true if the app requires bootstrapping
75 | func RequiresBootStrap() bool {
76 | if !fileExists(configPath()) {
77 | return true
78 | }
79 | return false
80 | }
81 |
82 | func configPath() string {
83 | return "secrets/fragmenta.json"
84 | }
85 |
86 | func projectPathRelative(projectPath string) string {
87 | goSrc := os.Getenv("GOPATH") + "/src/"
88 | return strings.Replace(projectPath, goSrc, "", 1)
89 | }
90 |
91 | func generateConfig(projectPath string) error {
92 | configPath := configPath()
93 | prefix := path.Base(projectPath)
94 | prefix = strings.Replace(prefix, "-", "_", -1)
95 | log.Printf("Generating new config at %s", configPath)
96 |
97 | ConfigProduction = map[string]string{}
98 | ConfigDevelopment = map[string]string{}
99 | ConfigTest = map[string]string{
100 | "port": "3000",
101 | "log": "log/test.log",
102 | "db_adapter": "postgres",
103 | "db": prefix + "_test",
104 | "db_user": prefix + "_server",
105 | "db_pass": randomKey(8),
106 | "assets_compiled": "no",
107 | "path": projectPathRelative(projectPath),
108 | "hmac_key": randomKey(32),
109 | "secret_key": randomKey(32),
110 | "session_name": prefix,
111 | }
112 |
113 | for k, v := range ConfigTest {
114 | ConfigDevelopment[k] = v
115 | ConfigProduction[k] = v
116 | }
117 | ConfigDevelopment["db"] = prefix + "_development"
118 | ConfigDevelopment["log"] = "log/development.log"
119 | ConfigDevelopment["hmac_key"] = randomKey(32)
120 | ConfigDevelopment["secret_key"] = randomKey(32)
121 |
122 | ConfigProduction["db"] = prefix + "_production"
123 | ConfigProduction["log"] = "log/production.log"
124 | ConfigProduction["port"] = "80" //FIXME set up for https with port 443
125 | ConfigProduction["assets_compiled"] = "yes"
126 | ConfigProduction["hmac_key"] = randomKey(32)
127 | ConfigProduction["secret_key"] = randomKey(32)
128 |
129 | configs := map[string]map[string]string{
130 | "production": ConfigProduction,
131 | "development": ConfigDevelopment,
132 | "test": ConfigTest,
133 | }
134 |
135 | configJSON, err := json.MarshalIndent(configs, "", "\t")
136 | if err != nil {
137 | log.Printf("Error parsing config %s %v", configPath, err)
138 | return err
139 | }
140 |
141 | // Write the config json file
142 | err = ioutil.WriteFile(configPath, configJSON, permissions)
143 | if err != nil {
144 | log.Printf("Error writing config %s %v", configPath, err)
145 | return err
146 | }
147 |
148 | return nil
149 | }
150 |
151 | // generateCreateSQL generates an SQL migration file to create the database user and database referred to in config
152 | func generateCreateSQL(projectPath string) error {
153 |
154 | // Set up a Create-Database migration, which comes first
155 | name := path.Base(projectPath)
156 | d := ConfigDevelopment["db"]
157 | u := ConfigDevelopment["db_user"]
158 | p := ConfigDevelopment["db_pass"]
159 | sql := fmt.Sprintf("/* Setup database for %s */\nCREATE USER \"%s\" WITH PASSWORD '%s';\nCREATE DATABASE \"%s\" WITH OWNER \"%s\";", name, u, p, d, u)
160 |
161 | // Generate a migration to create db with today's date
162 | file := migrationPath(projectPath, createDatabaseMigrationName)
163 | err := ioutil.WriteFile(file, []byte(sql), 0744)
164 | if err != nil {
165 | return err
166 | }
167 |
168 | // If we have a Create-Tables file, copy it out to a new migration with today's date
169 | createTablesPath := path.Join(projectPath, "db", "migrate", createTablesMigrationName+".sql.tmpl")
170 | if fileExists(createTablesPath) {
171 | sql, err := ioutil.ReadFile(createTablesPath)
172 | if err != nil {
173 | return err
174 | }
175 |
176 | // Now vivify the template, for now we just replace one key
177 | sqlString := strings.Replace(string(sql), "[[.fragmenta_db_user]]", u, -1)
178 |
179 | file = migrationPath(projectPath, createTablesMigrationName)
180 | err = ioutil.WriteFile(file, []byte(sqlString), 0744)
181 | if err != nil {
182 | return err
183 | }
184 | // Remove the old file
185 | os.Remove(createTablesPath)
186 |
187 | } else {
188 | fmt.Printf("NO TABLES %s", createTablesPath)
189 | }
190 |
191 | return nil
192 | }
193 |
194 | // runMigrations at projectPath
195 | func runMigrations(projectPath string) error {
196 | var migrations []string
197 | var migrationCount int
198 |
199 | config := ConfigDevelopment
200 |
201 | // Get a list of migration files
202 | files, err := filepath.Glob("./db/migrate/*.sql")
203 | if err != nil {
204 | return err
205 | }
206 |
207 | // Sort the list alphabetically
208 | sort.Strings(files)
209 |
210 | for _, file := range files {
211 | filename := path.Base(file)
212 |
213 | log.Printf("Running migration %s", filename)
214 |
215 | args := []string{"-d", config["db"], "-f", file}
216 | if strings.Contains(filename, createDatabaseMigrationName) {
217 | args = []string{"-f", file}
218 | log.Printf("Running database creation migration: %s", file)
219 | }
220 |
221 | // Execute this sql file against the database
222 | result, err := runCommand("psql", args...)
223 | if err != nil || strings.Contains(string(result), "ERROR") {
224 | if err == nil {
225 | err = fmt.Errorf("\n%s", string(result))
226 | }
227 |
228 | // If at any point we fail, log it and break
229 | log.Printf("ERROR loading sql migration:%s\n", err)
230 | log.Printf("All further migrations cancelled\n\n")
231 | return err
232 | }
233 |
234 | migrationCount++
235 | migrations = append(migrations, filename)
236 | log.Printf("Completed migration %s\n%s\n%s", filename, string(result), "-")
237 |
238 | }
239 |
240 | if migrationCount > 0 {
241 | writeMetadata(config, migrations)
242 | log.Printf("Migrations complete up to migration %v on db %s\n\n", migrations, config["db"])
243 | }
244 |
245 | return nil
246 | }
247 |
248 | // Oh, we need to write the full list of migrations, not just one migration version
249 |
250 | // Update the database with a line recording what we have done
251 | func writeMetadata(config map[string]string, migrations []string) {
252 | // Try opening the db (db may not exist at this stage)
253 | err := openDatabase(config)
254 | if err != nil {
255 | log.Printf("Database ERROR %s", err)
256 | }
257 | defer query.CloseDatabase()
258 |
259 | for _, m := range migrations {
260 | sql := "Insert into fragmenta_metadata(updated_at,fragmenta_version,migration_version,status) VALUES(NOW(),$1,$2,100);"
261 | result, err := query.ExecSQL(sql, fragmentaVersion, m)
262 | if err != nil {
263 | log.Printf("Database ERROR %s %s", err, result)
264 | }
265 | }
266 |
267 | }
268 |
269 | // Open our database
270 | func openDatabase(config map[string]string) error {
271 | // Open the database
272 | options := map[string]string{
273 | "adapter": config["db_adapter"],
274 | "user": config["db_user"],
275 | "password": config["db_pass"],
276 | "db": config["db"],
277 | // "debug" : "true",
278 | }
279 |
280 | err := query.OpenDatabase(options)
281 | if err != nil {
282 | return err
283 | }
284 |
285 | log.Printf("%s\n", "-")
286 | log.Printf("Opened database at %s for user %s", config["db"], config["db_user"])
287 | return nil
288 | }
289 |
290 | // Generate a suitable path for a migration from the current date/time down to nanosecond
291 | func migrationPath(path string, name string) string {
292 | now := time.Now()
293 | layout := "2006-01-02-150405"
294 | return fmt.Sprintf("%s/db/migrate/%s-%s.sql", path, now.Format(layout), name)
295 | }
296 |
297 | // Generate a random 32 byte key encoded in base64
298 | func randomKey(l int64) string {
299 | k := make([]byte, l)
300 | if _, err := io.ReadFull(rand.Reader, k); err != nil {
301 | return ""
302 | }
303 | return hex.EncodeToString(k)
304 | }
305 |
306 | // fileExists returns true if this file exists
307 | func fileExists(p string) bool {
308 | _, err := os.Stat(p)
309 | if err != nil && os.IsNotExist(err) {
310 | return false
311 | }
312 |
313 | return true
314 | }
315 |
316 | // runCommand runs a command with exec.Command
317 | func runCommand(command string, args ...string) ([]byte, error) {
318 |
319 | cmd := exec.Command(command, args...)
320 | output, err := cmd.CombinedOutput()
321 | if err != nil {
322 | return output, err
323 | }
324 |
325 | return output, nil
326 | }
327 |
--------------------------------------------------------------------------------
/src/users/actions/actions_test.go:
--------------------------------------------------------------------------------
1 | package useractions
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/fragmenta/auth"
11 | "github.com/fragmenta/query"
12 |
13 | "github.com/fragmenta/fragmenta-app/src/lib/resource"
14 | "github.com/fragmenta/fragmenta-app/src/users"
15 | )
16 |
17 | // names is used to test setting and getting the first string field of the user.
18 | var names = []string{"foo", "bar"}
19 |
20 | // TestSetup performs setup for integration tests
21 | // using the test database, real views, and mock authorisation
22 | // If we can run this once for global tests it might be more efficient?
23 | func TestSetup(t *testing.T) {
24 | err := resource.SetupTestDatabase(3)
25 | if err != nil {
26 | t.Fatalf("users: Setup db failed %s", err)
27 | }
28 |
29 | // Set up mock auth
30 | resource.SetupAuthorisation()
31 |
32 | // Load templates for rendering
33 | resource.SetupView(3)
34 |
35 | // Delete all users to ensure we get consistent results?
36 | query.ExecSQL("delete from users;")
37 | query.ExecSQL("ALTER SEQUENCE users_id_seq RESTART WITH 1;")
38 |
39 | // Insert a test user for checking logins
40 | query.ExecSQL("INSERT INTO users VALUES(1,NOW(),NOW(),'example@example.com','test',0,10,'$2a$10$2IUzpI/yH0Xc.qs9Z5UUL.3f9bqi0ThvbKs6Q91UOlyCEGY8hdBw6');")
41 |
42 | }
43 |
44 | // Test GET /users/create
45 | func TestShowCreateUser(t *testing.T) {
46 |
47 | // Create request context
48 | w, c := resource.GetRequestContext("/users/create", "/users/create", users.MockAdmin())
49 |
50 | // Run the handler
51 | err := HandleCreateShow(c)
52 |
53 | // Test the error response
54 | if err != nil || w.Code != http.StatusOK {
55 | t.Fatalf("useractions: error handling HandleCreateShow %s", err)
56 | }
57 |
58 | // Test the body for a known pattern
59 | pattern := "resource-update-form"
60 | if !strings.Contains(w.Body.String(), pattern) {
61 | t.Fatalf("useractions: unexpected response for HandleCreateShow expected:%s got:%s", pattern, w.Body.String())
62 | }
63 |
64 | }
65 |
66 | // Test POST /users/create
67 | func TestCreateUser(t *testing.T) {
68 | form := url.Values{}
69 | form.Add("name", names[0])
70 | body := strings.NewReader(form.Encode())
71 |
72 | // Create request context
73 | w, c := resource.PostRequestContext("/users/create", "/users/create", body, users.MockAdmin())
74 |
75 | // Run the handler to update the user
76 | err := HandleCreate(c)
77 | if err != nil {
78 | t.Fatalf("useractions: error handling HandleCreate %s", err)
79 | }
80 |
81 | // Test we get a redirect after update (to the user concerned)
82 | if w.Code != http.StatusFound {
83 | t.Fatalf("useractions: unexpected response code for HandleCreate expected:%d got:%d", http.StatusFound, w.Code)
84 | }
85 |
86 | // Check the user name is in now value names[1]
87 | user, err := users.Find(1)
88 | if err != nil {
89 | t.Fatalf("useractions: error finding created user %s", err)
90 | }
91 | if user.ID != 1 || user.Name != names[0] {
92 | t.Fatalf("useractions: error with created user values: %v", user)
93 | }
94 | }
95 |
96 | // Test GET /users
97 | func TestListUsers(t *testing.T) {
98 |
99 | // Create request context
100 | w, c := resource.GetRequestContext("/users", "/users", users.MockAdmin())
101 |
102 | // Run the handler
103 | err := HandleIndex(c)
104 |
105 | // Test the error response
106 | if err != nil || w.Code != http.StatusOK {
107 | t.Fatalf("useractions: error handling HandleIndex %s", err)
108 | }
109 |
110 | // Test the body for a known pattern
111 | pattern := "data-table-head"
112 | if !strings.Contains(w.Body.String(), pattern) {
113 | t.Fatalf("useractions: unexpected response for HandleIndex expected:%s got:%s", pattern, w.Body.String())
114 | }
115 |
116 | }
117 |
118 | // Test of GET /users/1
119 | func TestShowUser(t *testing.T) {
120 |
121 | // Create request context
122 | w, c := resource.GetRequestContext("/users/1", "/users/{id:[0-9]+}", users.MockAdmin())
123 |
124 | // Run the handler
125 | err := HandleShow(c)
126 |
127 | // Test the error response
128 | if err != nil || w.Code != http.StatusOK {
129 | t.Fatalf("useractions: error handling HandleShow %s", err)
130 | }
131 |
132 | // Test the body for a known pattern
133 | pattern := fmt.Sprintf("%s
", names[0])
134 | if !strings.Contains(w.Body.String(), pattern) {
135 | t.Fatalf("useractions: unexpected response for HandleShow expected:%s got:%s", pattern, w.Body.String())
136 | }
137 | }
138 |
139 | // Test GET /users/123/update
140 | func TestShowUpdateUser(t *testing.T) {
141 |
142 | // Create request context
143 | w, c := resource.GetRequestContext("/users/1/update", "/users/{id:[0-9]+}/update", users.MockAdmin())
144 |
145 | // Run the handler
146 | err := HandleUpdateShow(c)
147 |
148 | // Test the error response
149 | if err != nil || w.Code != http.StatusOK {
150 | t.Fatalf("useractions: error handling HandleCreateShow %s", err)
151 | }
152 |
153 | // Test the body for a known pattern
154 | pattern := "resource-update-form"
155 | if !strings.Contains(w.Body.String(), pattern) {
156 | t.Fatalf("useractions: unexpected response for HandleCreateShow expected:%s got:%s", pattern, w.Body.String())
157 | }
158 |
159 | }
160 |
161 | // Test POST /users/123/update
162 | func TestUpdateUser(t *testing.T) {
163 | form := url.Values{}
164 | form.Add("name", names[1])
165 | body := strings.NewReader(form.Encode())
166 |
167 | // Create request context
168 | w, c := resource.PostRequestContext("/users/1/update", "/users/{id:[0-9]+}/update", body, users.MockAdmin())
169 |
170 | // Run the handler to update the user
171 | err := HandleUpdate(c)
172 | if err != nil {
173 | t.Fatalf("useractions: error handling HandleUpdateUser %s", err)
174 | }
175 |
176 | // Test we get a redirect after update (to the user concerned)
177 | if w.Code != http.StatusFound {
178 | t.Fatalf("useractions: unexpected response code for HandleUpdateUser expected:%d got:%d", http.StatusFound, w.Code)
179 | }
180 |
181 | // Check the user name is in now value names[1]
182 | user, err := users.Find(1)
183 | if err != nil {
184 | t.Fatalf("useractions: error finding updated user %s", err)
185 | }
186 | if user.ID != 1 || user.Name != names[1] {
187 | t.Fatalf("useractions: error with updated user values: %v", user)
188 | }
189 |
190 | }
191 |
192 | // Test of POST /users/123/destroy
193 | func TestDeleteUser(t *testing.T) {
194 |
195 | body := strings.NewReader(``)
196 |
197 | // Test permissions - anon users can't destroy users
198 |
199 | // Create request context
200 | _, c := resource.PostRequestContext("/users/2/destroy", "/users/{id:[0-9]+}/destroy", body, users.MockAnon())
201 |
202 | // Run the handler to test failure as anon
203 | err := HandleDestroy(c)
204 | if err == nil { // failure expected
205 | t.Fatalf("useractions: unexpected response for HandleDestroy as anon, expected failure")
206 | }
207 |
208 | // Now test deleting the user created above as admin
209 | // Create request context
210 | w, c := resource.PostRequestContext("/users/1/destroy", "/users/{id:[0-9]+}/destroy", body, users.MockAdmin())
211 |
212 | // Run the handler
213 | err = HandleDestroy(c)
214 |
215 | // Test the error response is 302 StatusFound
216 | if err != nil {
217 | t.Fatalf("useractions: error handling HandleDestroy %s", err)
218 | }
219 |
220 | // Test we get a redirect after delete
221 | if w.Code != http.StatusFound {
222 | t.Fatalf("useractions: unexpected response code for HandleDestroy expected:%d got:%d", http.StatusFound, w.Code)
223 | }
224 |
225 | }
226 |
227 | // Test GET /users/login
228 | func TestShowLogin(t *testing.T) {
229 |
230 | // Create request context with admin user
231 | w, c := resource.GetRequestContext("/users/login", "/users/login", users.MockAdmin())
232 |
233 | // Run the handler
234 | err := HandleLoginShow(c)
235 |
236 | // Check for redirect as they are considered logged in
237 | if err != nil || w.Code != http.StatusFound {
238 | t.Fatalf("useractions: error handling HandleLoginShow %s", err)
239 | }
240 |
241 | // Create request context with anon user
242 | w, c = resource.GetRequestContext("/users/login", "/users/login", users.MockAnon())
243 |
244 | // Run the handler
245 | err = HandleLoginShow(c)
246 |
247 | // Test the error response
248 | if err != nil || w.Code != http.StatusOK {
249 | t.Fatalf("useractions: error handling HandleLoginShow %s", err)
250 | }
251 |
252 | // Test the body for a known pattern
253 | pattern := "password"
254 | if !strings.Contains(w.Body.String(), pattern) {
255 | t.Fatalf("useractions: unexpected response for HandleLoginShow expected:%s got:%s", pattern, w.Body.String())
256 | }
257 |
258 | }
259 |
260 | // Test POST /users/login
261 | func TestLogin(t *testing.T) {
262 |
263 | // These need to match entries in the test db for this to work
264 | form := url.Values{}
265 | form.Add("email", "example@example.com")
266 | form.Add("password", "Hunter2")
267 | body := strings.NewReader(form.Encode())
268 |
269 | // Test posting to the login link, we expect success as setup inserts this user
270 | w, c := resource.PostRequestContext("/users/1/destroy", "/users/{id:[0-9]+}/destroy", body, users.MockAnon())
271 |
272 | // Run the handler
273 | err := HandleLogin(c)
274 | if err != nil || w.Code != http.StatusFound {
275 | t.Fatalf("useractions: error on HandleLogin %s", err)
276 | }
277 |
278 | }
279 |
280 | // Test POST /users/logout
281 | func TestLogout(t *testing.T) {
282 |
283 | // Test posting to logout link to log the user out
284 | w, c := resource.PostRequestContext("/users/1/destroy", "/users/{id:[0-9]+}/destroy", nil, users.MockAnon())
285 |
286 | // Store something in the session
287 | session, err := auth.Session(w, c.Request())
288 | if err != nil {
289 | t.Fatalf("#error problem retrieving session")
290 | }
291 |
292 | // Set the cookie with user ID
293 | session.Set(auth.SessionUserKey, fmt.Sprintf("%d", 99))
294 | session.Save(w)
295 |
296 | // Run the handler
297 | err = HandleLogout(c)
298 | if err != nil {
299 | t.Fatalf("useractions: error on HandleLogout %s", err)
300 | }
301 |
302 | // Check we've set an empty session on this outgoing writer
303 | if !strings.Contains(string(w.Header().Get("Set-Cookie")), auth.SessionName+"=;") {
304 | t.Fatalf("useractions: error on HandleLogout - session not cleared")
305 | }
306 |
307 | // TODO - to better test this we should have an integration test with a server
308 |
309 | }
310 |
--------------------------------------------------------------------------------