github.com/decred/politeia@v1.4.0/politeiawww/legacy/testing.go (about) 1 // Copyright (c) 2017-2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package legacy 6 7 import ( 8 "encoding/hex" 9 "errors" 10 "os" 11 "path/filepath" 12 "testing" 13 14 "github.com/decred/dcrd/chaincfg/v3" 15 "github.com/decred/politeia/politeiad/api/v1/identity" 16 cms "github.com/decred/politeia/politeiawww/api/cms/v1" 17 www "github.com/decred/politeia/politeiawww/api/www/v1" 18 "github.com/decred/politeia/politeiawww/config" 19 "github.com/decred/politeia/politeiawww/legacy/mail" 20 "github.com/decred/politeia/politeiawww/legacy/sessions" 21 "github.com/decred/politeia/politeiawww/legacy/user" 22 "github.com/decred/politeia/politeiawww/legacy/user/localdb" 23 "github.com/decred/politeia/politeiawww/logger" 24 "github.com/decred/politeia/util" 25 "github.com/google/uuid" 26 "github.com/gorilla/mux" 27 ) 28 29 // errToStr returns the string representation of the error. If the error is a 30 // UserError then the human readable error message is returned instead of the 31 // error code. 32 func errToStr(e error) string { 33 if e == nil { 34 return "nil" 35 } 36 37 var userErr www.UserError 38 if errors.As(e, &userErr) { 39 return www.ErrorStatus[userErr.ErrorCode] 40 } 41 42 return e.Error() 43 } 44 45 // newUser creates a new user using randomly generated user credentials and 46 // inserts the user into the database. The user details and the full user 47 // identity are returned. 48 func newUser(t *testing.T, p *Politeiawww, isVerified, isAdmin bool) (*user.User, *identity.FullIdentity) { 49 t.Helper() 50 51 // Generate random bytes to be used as user credentials 52 r, err := util.Random(int(www.PolicyMinPasswordLength)) 53 if err != nil { 54 t.Fatalf("%v", err) 55 } 56 57 // Setup user 58 pass, err := p.hashPassword(hex.EncodeToString(r)) 59 if err != nil { 60 t.Fatalf("%v", err) 61 } 62 tokenb, expiry, err := newVerificationTokenAndExpiry() 63 if err != nil { 64 t.Fatalf("%v", err) 65 } 66 u := user.User{ 67 ID: uuid.New(), 68 Admin: isAdmin, 69 Email: hex.EncodeToString(r) + "@example.com", 70 Username: hex.EncodeToString(r), 71 HashedPassword: pass, 72 NewUserVerificationToken: tokenb, 73 NewUserVerificationExpiry: expiry, 74 } 75 fid, err := identity.New() 76 if err != nil { 77 t.Fatalf("%v", err) 78 } 79 pubkey := hex.EncodeToString(fid.Public.Key[:]) 80 id, err := user.NewIdentity(pubkey) 81 if err != nil { 82 t.Fatalf("%v", err) 83 } 84 err = u.AddIdentity(*id) 85 if err != nil { 86 t.Fatalf("%v", err) 87 } 88 if isVerified { 89 u.NewUserVerificationToken = nil 90 u.NewUserVerificationExpiry = 0 91 err := u.ActivateIdentity(id.Key[:]) 92 if err != nil { 93 t.Fatalf("%v", err) 94 } 95 } 96 97 // Add user to database 98 err = p.db.UserNew(u) 99 if err != nil { 100 t.Fatalf("%v", err) 101 } 102 103 // Add the user to the politeiawww in-memory [email]userID 104 // cache. Since the userID is generated in the database layer 105 // we need to lookup the user in order to get the userID. 106 usr, err := p.db.UserGetByUsername(u.Username) 107 if err != nil { 108 t.Fatalf("%v", err) 109 } 110 p.setUserEmailsCache(usr.Email, usr.ID) 111 112 // Add paywall info to the user record 113 err = p.generateNewUserPaywall(usr) 114 if err != nil { 115 t.Fatalf("%v", err) 116 } 117 118 // Lookup user record one more time so that 119 // we return a user object with the paywall 120 // details filled in. 121 usr, err = p.db.UserGetByUsername(u.Username) 122 if err != nil { 123 t.Fatalf("%v", err) 124 } 125 126 return usr, fid 127 } 128 129 // newCMSUser creates a new user using randomly generated user credentials and 130 // inserts the user into the database. The user details and the full user 131 // identity are returned. 132 func newCMSUser(t *testing.T, p *Politeiawww, isAdmin bool, setGithubname bool, domain cms.DomainTypeT, contractorType cms.ContractorTypeT) *user.CMSUser { 133 t.Helper() 134 135 // Generate random bytes to be used as user credentials 136 r, err := util.Random(int(www.PolicyMinPasswordLength)) 137 if err != nil { 138 t.Fatalf("%v", err) 139 } 140 141 u := user.NewCMSUser{ 142 Email: hex.EncodeToString(r) + "@example.com", 143 Username: hex.EncodeToString(r), 144 NewUserVerificationToken: nil, 145 NewUserVerificationExpiry: 0, 146 ContractorType: int(cms.ContractorTypeDirect), 147 } 148 149 payload, err := user.EncodeNewCMSUser(u) 150 if err != nil { 151 t.Errorf("unable to encode new user %v", err) 152 } 153 pc := user.PluginCommand{ 154 ID: user.CMSPluginID, 155 Command: user.CmdNewCMSUser, 156 Payload: string(payload), 157 } 158 _, err = p.db.PluginExec(pc) 159 if err != nil { 160 t.Errorf("unable to execute new cms user db request %v", err) 161 } 162 163 usr, err := p.db.UserGetByUsername(u.Username) 164 if err != nil { 165 t.Fatalf("error getting user by username %v", err) 166 } 167 p.setUserEmailsCache(usr.Email, usr.ID) 168 169 if isAdmin { 170 usr.Admin = true 171 err := p.db.UserUpdate(*usr) 172 if err != nil { 173 t.Fatalf("error updating user for admin %v", err) 174 } 175 } 176 177 uu := user.UpdateCMSUser{ 178 ID: usr.ID, 179 Domain: int(domain), 180 ContractorType: int(contractorType), 181 } 182 if setGithubname { 183 uu.GitHubName = "testGithub" 184 } 185 payload, err = user.EncodeUpdateCMSUser(uu) 186 if err != nil { 187 t.Errorf("unable to encode new user %v", err) 188 } 189 pc = user.PluginCommand{ 190 ID: user.CMSPluginID, 191 Command: user.CmdUpdateCMSUser, 192 Payload: string(payload), 193 } 194 _, err = p.db.PluginExec(pc) 195 if err != nil { 196 t.Errorf("unable to execute update cms user db request %v", err) 197 } 198 199 // Add the user to the politeiawww in-memory [email]userID 200 // cache. Since the userID is generated in the database layer 201 // we need to lookup the user in order to get the userID. 202 usr, err = p.db.UserGetByUsername(u.Username) 203 if err != nil { 204 t.Fatalf("%v", err) 205 } 206 p.setUserEmailsCache(usr.Email, usr.ID) 207 208 cmsUser, err := p.getCMSUserByIDRaw(usr.ID.String()) 209 if err != nil { 210 t.Fatalf("%v", err) 211 } 212 213 return cmsUser 214 } 215 216 // newTestPoliteiawww returns a new politeiawww context that is setup for 217 // testing and a closure that cleans up the test environment when invoked. 218 func newTestPoliteiawww(t *testing.T) (*Politeiawww, func()) { 219 t.Helper() 220 221 // Make a temp directory for test data. Temp directory 222 // is removed in the returned closure. 223 dataDir, err := os.MkdirTemp("", "politeiawww.test") 224 if err != nil { 225 t.Fatalf("open tmp dir: %v", err) 226 } 227 228 // Setup logging 229 // initLogRotator(filepath.Join(dataDir, "politeiawww.test.log")) 230 // setLogLevels("off") 231 232 // Setup config 233 xpub := "tpubVobLtToNtTq6TZNw4raWQok35PRPZou53vegZqNubtBTJMMFm" + 234 "uMpWybFCfweJ52N8uZJPZZdHE5SRnBBuuRPfC5jdNstfKjiAs8JtbYG9jx" 235 fid, err := identity.New() 236 if err != nil { 237 t.Fatal(err) 238 } 239 cfg := &config.Config{ 240 DataDir: dataDir, 241 TestNet: true, 242 LegacyConfig: config.LegacyConfig{ 243 PaywallAmount: 1e7, 244 PaywallXpub: xpub, 245 VoteDurationMin: 2016, 246 VoteDurationMax: 4032, 247 }, 248 Identity: &fid.Public, 249 } 250 251 // Setup database 252 db, err := localdb.New(filepath.Join(cfg.DataDir, "localdb")) 253 if err != nil { 254 t.Fatalf("setup database: %v", err) 255 } 256 257 // Setup mail client 258 mailClient, err := mail.NewClient("", "", "", "", "", false, 0, db) 259 if err != nil { 260 t.Fatal(err) 261 } 262 263 // Setup sessions 264 cookieKey, err := util.Random(32) 265 if err != nil { 266 t.Fatalf("create cookie key: %v", err) 267 } 268 269 // Setup politeiawww context 270 p := Politeiawww{ 271 cfg: cfg, 272 params: chaincfg.TestNet3Params(), 273 router: mux.NewRouter(), 274 auth: mux.NewRouter(), 275 sessions: sessions.New(db, cookieKey), 276 mail: mailClient, 277 db: db, 278 test: true, 279 userEmails: make(map[string]uuid.UUID), 280 userPaywallPool: make(map[uuid.UUID]paywallPoolMember), 281 } 282 283 // Setup routes 284 p.setUserWWWRoutes() 285 286 // The cleanup is handled using a closure so that the temp dir 287 // can be deleted using the local variable and not cfg.DataDir. 288 // Using cfg.DataDir could be misused and lead to the deletion 289 // of an unintended directory. 290 return &p, func() { 291 t.Helper() 292 293 err := db.Close() 294 if err != nil { 295 t.Fatalf("close db: %v", err) 296 } 297 298 /* 299 err = logRotator.Close() 300 if err != nil { 301 t.Fatalf("close log rotator: %v", err) 302 } 303 */ 304 305 err = os.RemoveAll(dataDir) 306 if err != nil { 307 t.Fatalf("remove tmp dir: %v", err) 308 } 309 } 310 } 311 312 // newTestCMSwww returns a new cmswww context that is setup for 313 // testing and a closure that cleans up the test environment when invoked. 314 func newTestCMSwww(t *testing.T) (*Politeiawww, func()) { 315 t.Helper() 316 317 // Make a temp directory for test data. Temp directory 318 // is removed in the returned closure. 319 dataDir, err := os.MkdirTemp("", "cmswww.test") 320 if err != nil { 321 t.Fatalf("open tmp dir: %v", err) 322 } 323 324 // Turn logging off 325 logger.InitLogRotator(filepath.Join(dataDir, "cmswww.test.log")) 326 logger.SetLogLevels("off") 327 328 // Setup config 329 xpub := "tpubVobLtToNtTq6TZNw4raWQok35PRPZou53vegZqNubtBTJMMFm" + 330 "uMpWybFCfweJ52N8uZJPZZdHE5SRnBBuuRPfC5jdNstfKjiAs8JtbYG9jx" 331 cfg := &config.Config{ 332 DataDir: dataDir, 333 TestNet: true, 334 LegacyConfig: config.LegacyConfig{ 335 PaywallAmount: 1e7, 336 PaywallXpub: xpub, 337 VoteDurationMin: 2016, 338 VoteDurationMax: 4032, 339 Mode: config.CMSWWWMode, 340 }, 341 } 342 343 // Setup database 344 db, err := localdb.New(filepath.Join(cfg.DataDir, "localdb")) 345 if err != nil { 346 t.Fatalf("setup database: %v", err) 347 } 348 349 // Register cms userdb plugin 350 plugin := user.Plugin{ 351 ID: user.CMSPluginID, 352 Version: user.CMSPluginVersion, 353 } 354 err = db.RegisterPlugin(plugin) 355 if err != nil { 356 t.Fatalf("register userdb plugin: %v", err) 357 } 358 359 // Setup smtp 360 mailClient, err := mail.NewClient("", "", "", "", "", false, 0, db) 361 if err != nil { 362 t.Fatalf("setup SMTP: %v", err) 363 } 364 365 // Setup sessions 366 cookieKey, err := util.Random(32) 367 if err != nil { 368 t.Fatalf("create cookie key: %v", err) 369 } 370 371 // Create politeiawww context 372 p := Politeiawww{ 373 cfg: cfg, 374 db: db, 375 params: chaincfg.TestNet3Params(), 376 router: mux.NewRouter(), 377 auth: mux.NewRouter(), 378 sessions: sessions.New(db, cookieKey), 379 mail: mailClient, 380 test: true, 381 userEmails: make(map[string]uuid.UUID), 382 userPaywallPool: make(map[uuid.UUID]paywallPoolMember), 383 } 384 385 // Setup routes 386 p.setCMSWWWRoutes() 387 p.setCMSUserWWWRoutes() 388 389 // The cleanup is handled using a closure so that the temp dir 390 // can be deleted using the local variable and not cfg.DataDir. 391 // Using cfg.DataDir could be misused and lead to the deletion 392 // of an unintended directory. 393 return &p, func() { 394 t.Helper() 395 396 err := db.Close() 397 if err != nil { 398 t.Fatalf("close db: %v", err) 399 } 400 401 /* 402 err = logRotator.Close() 403 if err != nil { 404 t.Fatalf("close log rotator: %v", err) 405 } 406 */ 407 408 err = os.RemoveAll(dataDir) 409 if err != nil { 410 t.Fatalf("remove tmp dir: %v", err) 411 } 412 } 413 }