Transaction
17554817E5D721…F6814EB6F6A6
Block 747,745 · index 0 · indexed
Summary
- Hash
- 17554817E5D721FEC5A9F1F9741B0216F5FA1169568A684E3739F6814EB6F6A6
- Block
- 747,745
- Size
- 31394 bytes
- Gas used
- 45,571,730 / 91,143,400
- Fee
- 91144 ugnot
- Status
- success
Messages
- Package
- gno.land/r/akkadia/beta/user
Arguments · 17
- #1user
- #2config.gno
- #3package user import ( "chain" "strconv" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/r/akkadia/beta/admin" ) const ( SetListLimitEvent = "SetListLimit" SetFrozenEvent = "SetFrozen" ) var ( listLimit int = 100 // Max items per list query isFrozen = true ) func SetListLimit(cur realm, limit int) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) if limit < 1 { panic("limit must be at least 1") } oldLimit := listLimit listLimit = limit chain.Emit( SetListLimitEvent, "oldLimit", strconv.Itoa(oldLimit), "newLimit", strconv.Itoa(limit), ) } func GetListLimit() int { return listLimit } // SetFrozen controls direct user creation and deletion only. func SetFrozen(cur realm, frozen bool) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) oldFrozen := isFrozen isFrozen = frozen chain.Emit( SetFrozenEvent, "oldFrozen", strconv.FormatBool(oldFrozen), "newFrozen", strconv.FormatBool(frozen), ) }
- #4gnomod.toml
- #5module = "gno.land/r/akkadia/beta/user" gno = "0.9"
- #6render.gno
- #7package user import ( "net/url" "strconv" "strings" "gno.land/p/jeronimoalbi/pager/v0" "gno.land/r/akkadia/beta/admin" ) // Render handles RESTful routing and returns Markdown responses. func Render(iUrl string) string { assertRenderURLLen(iUrl) u, err := url.Parse(iUrl) if err != nil { return "404\n" } query := u.Query() switch u.Path { case "": return renderHome() case "users": return renderUsersList(iUrl) case "user": addr := address(query.Get("address")) return renderUser(iUrl, addr) default: return renderError("Not found") } } func renderHome() string { output := `# AKKADIA User ## Introduction User realm stores address-keyed profiles for AKKADIA. - **created_at**: recorded when the user is created - **properties**: admin-managed key/value attributes ## Stats ` output += "* **Total Users**: " + strconv.Itoa(GetTotalUserSize()) + "\n" output += ` ## Quick Links * [Browse Users](./user:users) ## User Lookup <gno-form path="user"> <gno-input name="address" placeholder="Enter the address" /> </gno-form> ` return output } func renderUsersList(iUrl string) string { output := "# Users\n\n" output += "[← Home](./user:)\n\n" totalUsers := GetTotalUserSize() if totalUsers == 0 { output += "*No users registered yet.*\n" return output } pages, err := pager.New(iUrl, totalUsers, pager.WithPageSize(20)) if err != nil { return renderError("Invalid page") } currentPage := pages.Page() addrStrs := ListUsers(currentPage, pages.PageSize()) for _, addrStr := range addrStrs { nickname := "" listed := ListUsersByAddresses(address(addrStr)) if len(listed) > 0 { nickname = listed[0]["nickname"] } if nickname != "" { output += "* [" + escapeMarkdown(nickname) + " (" + addrStr + ")](./user:user?address=" + addrStr + "&page=" + strconv.Itoa(currentPage) + ")\n" } else { output += "* [" + addrStr + "](./user:user?address=" + addrStr + "&page=" + strconv.Itoa(currentPage) + ")\n" } } if pages.HasPages() { output += "\n" + pager.Picker(pages) } return output } func renderUser(iUrl string, addr address) string { if !addr.IsValid() { return renderError("Invalid address") } u, _ := url.Parse(iUrl) page := normalizeRenderPage(u.Query().Get("page")) addrStr := addr.String() if !HasUser(addr) { return renderError("User not found") } users := ListUsersByAddresses(addr) output := "# User Detail\n\n" if page != "" { output += "[← Back to List](./user:users?page=" + page + ")\n\n" } else { output += "[← Home](./user:)\n\n" } output += "* **Address**: " + addrStr + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + addrStr + ")]\n\n" if len(users) > 0 { output += "## Properties\n\n" for k, v := range users[0] { if k == "address" { continue } output += "* **" + escapeMarkdown(k) + "**: " + escapeMarkdown(v) + "\n" } } return output } func normalizeRenderPage(rawPage string) string { if rawPage == "" { return "" } page, err := strconv.Atoi(rawPage) if err != nil || page < 1 { return "" } return strconv.Itoa(page) } func renderError(message string) string { output := "# Error\n\n" output += "> " + message + "\n\n" output += "[← Back to Home](./user:)\n" return output } func escapeMarkdown(s string) string { s = strings.ReplaceAll(s, "\\", "\\\\") s = strings.ReplaceAll(s, "\n", "\\n") s = strings.ReplaceAll(s, "`", "\\`") s = strings.ReplaceAll(s, "*", "\\*") s = strings.ReplaceAll(s, "_", "\\_") s = strings.ReplaceAll(s, "{", "\\{") s = strings.ReplaceAll(s, "}", "\\}") s = strings.ReplaceAll(s, "[", "\\[") s = strings.ReplaceAll(s, "]", "\\]") s = strings.ReplaceAll(s, "(", "\\(") s = strings.ReplaceAll(s, ")", "\\)") s = strings.ReplaceAll(s, "#", "\\#") s = strings.ReplaceAll(s, "+", "\\+") s = strings.ReplaceAll(s, "-", "\\-") s = strings.ReplaceAll(s, ".", "\\.") s = strings.ReplaceAll(s, "!", "\\!") s = strings.ReplaceAll(s, "|", "\\|") s = strings.ReplaceAll(s, ">", "\\>") return s }
- #8render_test.gno
- #9package user import ( "net/url" "strings" "testing" "gno.land/p/nt/uassert/v0" ) func TestRenderEscapesMarkdownSensitiveUserValues(cur realm, t *testing.T) { setupUserTests(cross(cur), t) nickname := "N [x](y) **b**" intro := "**bold** [link](https://example.invalid)\n# heading" createUserForTest(cross(cur), regularUser) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AdminUpdateUser(cross(cur), regularUser, "nickname,introduction", url.PathEscape(nickname)+","+url.PathEscape(intro)) detail := Render("user?address=" + regularUser.String()) uassert.True(t, strings.Contains(detail, `N \[x\]\(y\) \*\*b\*\*`), "nickname should be escaped") uassert.True(t, strings.Contains(detail, `\*\*bold\*\* \[link\]\(https://example\.invalid\)\n\# heading`), "property value should be escaped") uassert.False(t, strings.Contains(detail, "[link](https://example.invalid)"), "property link syntax should not remain raw") uassert.False(t, strings.Contains(detail, "\n# heading"), "property newline heading should not remain raw") list := Render("users") uassert.True(t, strings.Contains(list, `N \[x\]\(y\) \*\*b\*\*`), "list nickname should be escaped") uassert.False(t, strings.Contains(list, "[profile](profile-page)"), "list nickname link syntax should not remain raw") } func TestRenderErrors(cur realm, t *testing.T) { setupUserTests(cross(cur), t) invalid := Render("user?address=invalid-address") uassert.True(t, strings.Contains(invalid, "Invalid address"), "render output should contain expected error") missing := Render("user?address=" + regularUser.String()) uassert.True(t, strings.Contains(missing, "User not found"), "missing user should render not found") unknown := Render("nickname?nickname=alice") uassert.True(t, strings.Contains(unknown, "Not found"), "removed nickname route should 404") } func TestRenderUserSanitizesPageLink(cur realm, t *testing.T) { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) valid := Render("user?address=" + regularUser.String() + "&page=02") uassert.True(t, strings.Contains(valid, "[← Back to List](./user:users?page=2)"), "valid page should be normalized") injected := Render("user?address=" + regularUser.String() + "&page=1%29%5D%28https%3A%2F%2Fexample.invalid%29") uassert.False(t, strings.Contains(injected, "example.invalid"), "invalid page should not be reflected") uassert.False(t, strings.Contains(injected, "users?page="), "invalid page should not render a page back link") uassert.True(t, strings.Contains(injected, "[← Home](./user:)"), "invalid page should fall back to home link") } func TestRenderRejectsOversizedURL(cur realm, t *testing.T) { uassert.PanicsWithMessage(t, cur, "url exceeds max runes", func() { Render(strings.Repeat("a", renderURLMaxRunes+1)) }) Render(strings.Repeat("a", renderURLMaxRunes)) } func TestRenderHomeUsesRelativeBrowseUsersLink(cur realm, t *testing.T) { setupUserTests(cross(cur), t) output := Render("") uassert.True(t, strings.Contains(output, "* [Browse Users](./user:users)"), "should use package-relative users link") uassert.False(t, strings.Contains(output, "* [Browse Users](users)"), "should not use path-relative users link") uassert.False(t, strings.Contains(output, "* [Browse Users](/r/akkadia/user:users)"), "should not use absolute users link") }
- #10user.gno
- #11package user import ( "chain" "net/url" "strconv" "time" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/p/nt/bptree/v0" "gno.land/r/akkadia/beta/admin" ) const ( CreateUserEvent = "CreateUser" UpdateUserEvent = "UpdateUser" DeleteUserEvent = "DeleteUser" createdAtKey = "created_at" ) // users is address string -> map[string]string. // The zero value is an empty BPTree with fanout 32. var users bptree.BPTree func HasUser(addr address) bool { assertIsValidAddress(addr) return users.Has(addr.String()) } func CreateUser(cur realm) { addr := accesscontrol.MustGetUserCaller(0, cur) if isFrozen { panic("user self-service is frozen") } addrStr := addr.String() if users.Has(addrStr) { panic("user already exists: " + addrStr) } users.Set(addrStr, map[string]string{ createdAtKey: strconv.FormatInt(time.Now().Unix(), 10), }) chain.Emit(CreateUserEvent, "address", addrStr) } // AdminCreateUser is idempotent so an operator can safely retry registration. func AdminCreateUser(cur realm, addr address) { accesscontrol.AssertIsAdminOrOperator(0, cur, admin.IsAdmin, admin.IsOperator) assertIsValidAddress(addr) addrStr := addr.String() if users.Has(addrStr) { return } users.Set(addrStr, map[string]string{ createdAtKey: strconv.FormatInt(time.Now().Unix(), 10), }) chain.Emit(CreateUserEvent, "address", addrStr) } func DeleteUser(cur realm) { addr := accesscontrol.MustGetUserCaller(0, cur) if isFrozen { panic("user self-service is frozen") } deleteUser(addr) } func AdminDeleteUser(cur realm, addr address) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) assertIsValidAddress(addr) deleteUser(addr) } // DeleteAll removes registrations while preserving freeze and list settings. func DeleteAll(cur realm) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) users = bptree.BPTree{} } func AdminUpdateUser(cur realm, addr address, propKeys, propValues string) { accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin) assertIsValidAddress(addr) addrStr := addr.String() current := users.Get(addrStr) if current == nil { panic("user not found: " + addrStr) } props := copyStringMap(current.(map[string]string)) updates := parsePropertiesCSV(propKeys, propValues) for key, value := range updates { props[key] = value } users.Set(addrStr, props) chain.Emit(UpdateUserEvent, "address", addrStr) } func GetTotalUserSize() int { return users.Size() } func ListUsers(page, count int) []string { assertListPageCount(page, count) result := []string{} offset := (page - 1) * count users.IterateByOffset(offset, count, func(key string, _ any) bool { result = append(result, key) return false }) return result } func ListUsersByAddresses(addrs ...address) []map[string]string { assertListLimit("addrs", len(addrs)) result := []map[string]string{} for _, addr := range addrs { assertIsValidAddress(addr) addrStr := addr.String() current := users.Get(addrStr) if current == nil { continue } props := copyStringMap(current.(map[string]string)) props["address"] = addrStr result = append(result, props) } return result } func deleteUser(addr address) { addrStr := addr.String() if _, removed := users.Remove(addrStr); !removed { panic("user not found: " + addrStr) } chain.Emit(DeleteUserEvent, "address", addrStr) } func copyStringMap(source map[string]string) map[string]string { result := map[string]string{} for key, value := range source { result[key] = value } return result } // parsePropertiesCSV accepts comma-separated keys and comma-separated values. // Each value must be URL-encoded so commas and other separators can be carried // safely; this parser URL-decodes values before returning the map. func parsePropertiesCSV(keys, values string) map[string]string { if keys == "" { panic("properties keys must be not empty") } result := map[string]string{} keyStart, valStart := 0, 0 keyIdx, valIdx := 0, 0 for { for keyIdx < len(keys) && keys[keyIdx] != ',' { keyIdx++ } for valIdx < len(values) && values[valIdx] != ',' { valIdx++ } key := keys[keyStart:keyIdx] if key == "" { panic("properties key must be not empty") } if _, found := result[key]; found { panic("properties key duplicated: " + key) } value, err := url.PathUnescape(values[valStart:valIdx]) if err != nil { panic("properties value URL decode failed: " + key) } result[key] = value keyEnd := keyIdx >= len(keys) valEnd := valIdx >= len(values) if keyEnd != valEnd { panic("properties keys and values count mismatch") } if keyEnd { break } keyIdx++ valIdx++ keyStart = keyIdx valStart = valIdx } return result } func assertIsValidAddress(addr address) { if !addr.IsValid() { panic("invalid address: " + addr.String()) } } func assertListLimit(field string, count int) { if count > listLimit { panic(field + " exceeds listLimit (max: " + strconv.Itoa(listLimit) + ")") } } func assertListPageCount(page int, count int) { if page < 1 { panic("page must be at least 1") } if count < 1 { panic("count must be at least 1") } assertListLimit("count", count) }
- #12user_test.gno
- #13package user import ( "testing" "time" "gno.land/p/nt/bptree/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/akkadia/beta/admin" ) var ( testAdmin = testutils.TestAddress("testAdmin") regularUser = testutils.TestAddress("regularUser") badUser = testutils.TestAddress("badUser") anotherUser = testutils.TestAddress("anotherUser") setupDone = false ) func setupUserTests(cur realm, t *testing.T) { t.Helper() users = bptree.BPTree{} listLimit = 100 isFrozen = true if setupDone { return } originalAdmin := admin.GetAdmin() testing.SetOriginCaller(originalAdmin) testing.SetRealm(testing.NewUserRealm(originalAdmin)) admin.TransferAdmin(cross(cur), testAdmin) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.AcceptAdmin(cross(cur)) setupDone = true } func createUserForTest(cur realm, addr address) { testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AdminCreateUser(cross(cur), addr) } func TestParsePropertiesCSV(cur realm, t *testing.T) { props := parsePropertiesCSV("nickname,introduction", "Alice,hello%2C%20world") uassert.Equal(t, "Alice", props["nickname"]) uassert.Equal(t, "hello, world", props["introduction"]) props = parsePropertiesCSV("nickname,introduction", "Alice,") uassert.Equal(t, "", props["introduction"]) uassert.PanicsWithMessage(t, cur, "properties keys must be not empty", func() { parsePropertiesCSV("", "") }) uassert.PanicsWithMessage(t, cur, "properties key duplicated: nickname", func() { parsePropertiesCSV("nickname,nickname", "Alice,Bob") }) uassert.PanicsWithMessage(t, cur, "properties keys and values count mismatch", func() { parsePropertiesCSV("nickname,introduction", "Alice") }) uassert.PanicsWithMessage(t, cur, "properties value URL decode failed: introduction", func() { parsePropertiesCSV("nickname,introduction", "Alice,%zz") }) } func TestCreateUser(cur realm, t *testing.T) { setupUserTests(cross(cur), t) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) SetFrozen(cross(cur), false) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewUserRealm(regularUser)) CreateUser(cross(cur)) uassert.True(t, HasUser(regularUser)) uassert.False(t, HasUser(testAdmin)) uassert.Equal(t, 1, GetTotalUserSize()) createdAt := ListUsersByAddresses(regularUser)[0][createdAtKey] uassert.True(t, createdAt != "") uassert.AbortsWithMessage(t, cur, "user already exists: "+regularUser.String(), func() { CreateUser(cross(cur)) }) uassert.Equal(t, createdAt, ListUsersByAddresses(regularUser)[0][createdAtKey]) // Operator delivery after self-registration remains a safe no-op. createUserForTest(cross(cur), regularUser) uassert.Equal(t, 1, GetTotalUserSize()) uassert.Equal(t, createdAt, ListUsersByAddresses(regularUser)[0][createdAtKey]) } func TestCreateUserRejectsCodeRealm(cur realm, t *testing.T) { setupUserTests(cross(cur), t) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) SetFrozen(cross(cur), false) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewCodeRealm("gno.land/r/akkadia/beta/user_relay")) uassert.AbortsWithMessage(t, cur, "contract calls not allowed: function must be called directly by user", func() { CreateUser(cross(cur)) }) uassert.Equal(t, 0, GetTotalUserSize()) } func TestAdminCreateUser(cur realm, t *testing.T) { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) uassert.True(t, HasUser(regularUser)) uassert.Equal(t, 1, GetTotalUserSize()) usersByAddr := ListUsersByAddresses(regularUser) uassert.Equal(t, 1, len(usersByAddr)) uassert.Equal(t, regularUser.String(), usersByAddr[0]["address"]) uassert.True(t, usersByAddr[0][createdAtKey] != "") createdAt := usersByAddr[0][createdAtKey] ctx := testing.GetContext() ctx.Time = ctx.Time.Add(time.Hour) testing.SetContext(ctx) createUserForTest(cross(cur), regularUser) uassert.Equal(t, 1, GetTotalUserSize()) uassert.Equal(t, createdAt, ListUsersByAddresses(regularUser)[0][createdAtKey]) } func TestAdminCreateUserOperator(cur realm, t *testing.T) { setupUserTests(cross(cur), t) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.SetOperator(cross(cur), anotherUser) testing.SetOriginCaller(anotherUser) testing.SetRealm(testing.NewUserRealm(anotherUser)) AdminCreateUser(cross(cur), regularUser) uassert.True(t, HasUser(regularUser)) uassert.False(t, HasUser(anotherUser)) uassert.Equal(t, 1, GetTotalUserSize()) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.UnsetOperator(cross(cur), anotherUser) } func TestAdminCreateUserRejectsUnauthorizedAndInvalidAddress(cur realm, t *testing.T) { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewUserRealm(regularUser)) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { AdminCreateUser(cross(cur), anotherUser) }) // Authorization must run even when the target already exists. uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { AdminCreateUser(cross(cur), regularUser) }) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/akkadia/beta/user_relay")) uassert.AbortsWithMessage(t, cur, "admin or operator access required", func() { AdminCreateUser(cross(cur), anotherUser) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) uassert.AbortsWithMessage(t, cur, "invalid address: "+address("").String(), func() { AdminCreateUser(cross(cur), address("")) }) uassert.True(t, HasUser(regularUser)) uassert.False(t, HasUser(anotherUser)) uassert.Equal(t, 1, GetTotalUserSize()) } func TestDeleteUser(cur realm, t *testing.T) { setupUserTests(cross(cur), t) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) SetFrozen(cross(cur), false) createUserForTest(cross(cur), regularUser) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewUserRealm(regularUser)) DeleteUser(cross(cur)) uassert.False(t, HasUser(regularUser)) uassert.Equal(t, 0, GetTotalUserSize()) uassert.AbortsWithMessage(t, cur, "user not found: "+regularUser.String(), func() { DeleteUser(cross(cur)) }) } func TestAdminDeleteUser(cur realm, t *testing.T) { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewUserRealm(regularUser)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { AdminDeleteUser(cross(cur), regularUser) }) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AdminDeleteUser(cross(cur), regularUser) uassert.False(t, HasUser(regularUser)) uassert.AbortsWithMessage(t, cur, "user not found: "+regularUser.String(), func() { AdminDeleteUser(cross(cur), regularUser) }) } func TestAdminUpdateUser(cur realm, t *testing.T) { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) createdAt := ListUsersByAddresses(regularUser)[0][createdAtKey] testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AdminUpdateUser(cross(cur), regularUser, "nickname,introduction", "Alice,hello%2C%20world") listed := ListUsersByAddresses(regularUser)[0] uassert.Equal(t, "Alice", listed["nickname"]) uassert.Equal(t, "hello, world", listed["introduction"]) uassert.Equal(t, createdAt, listed[createdAtKey]) AdminUpdateUser(cross(cur), regularUser, createdAtKey, "1") uassert.Equal(t, "1", ListUsersByAddresses(regularUser)[0][createdAtKey]) uassert.Equal(t, "Alice", ListUsersByAddresses(regularUser)[0]["nickname"]) } func TestAdminUpdateUserRejectsMissingAndUnauthorized(cur realm, t *testing.T) { setupUserTests(cross(cur), t) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) uassert.AbortsWithMessage(t, cur, "user not found: "+regularUser.String(), func() { AdminUpdateUser(cross(cur), regularUser, "nickname", "Alice") }) createUserForTest(cross(cur), regularUser) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewUserRealm(regularUser)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { AdminUpdateUser(cross(cur), regularUser, "nickname", "Alice") }) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/akkadia/beta/user_admin_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { AdminUpdateUser(cross(cur), regularUser, "nickname", "Alice") }) } func TestListUsersAddressOrder(cur realm, t *testing.T) { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) createUserForTest(cross(cur), anotherUser) listed := ListUsers(1, 10) uassert.Equal(t, 2, len(listed)) if regularUser.String() < anotherUser.String() { uassert.Equal(t, regularUser.String(), listed[0]) uassert.Equal(t, anotherUser.String(), listed[1]) } else { uassert.Equal(t, anotherUser.String(), listed[0]) uassert.Equal(t, regularUser.String(), listed[1]) } firstPage := ListUsers(1, 1) uassert.Equal(t, 1, len(firstPage)) uassert.Equal(t, listed[0], firstPage[0]) secondPage := ListUsers(2, 1) uassert.Equal(t, listed[1], secondPage[0]) } func TestListUsersByAddresses(cur realm, t *testing.T) { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AdminUpdateUser(cross(cur), regularUser, "nickname", "Alice") listed := ListUsersByAddresses(regularUser, badUser) uassert.Equal(t, 1, len(listed)) uassert.Equal(t, "Alice", listed[0]["nickname"]) listed[0]["nickname"] = "mutated" again := ListUsersByAddresses(regularUser) uassert.Equal(t, "Alice", again[0]["nickname"]) } func TestListInputValidation(cur realm, t *testing.T) { setupUserTests(cross(cur), t) uassert.PanicsWithMessage(t, cur, "page must be at least 1", func() { ListUsers(0, 1) }) uassert.PanicsWithMessage(t, cur, "count must be at least 1", func() { ListUsers(1, 0) }) uassert.PanicsWithMessage(t, cur, "count exceeds listLimit (max: 100)", func() { ListUsers(1, 101) }) uassert.PanicsWithMessage(t, cur, "invalid address: "+address("").String(), func() { HasUser(address("")) }) uassert.Equal(t, 100, GetListLimit()) } func TestSetListLimit(cur realm, t *testing.T) { setupUserTests(cross(cur), t) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) SetListLimit(cross(cur), 50) uassert.Equal(t, 50, GetListLimit()) SetListLimit(cross(cur), 1) uassert.PanicsWithMessage(t, cur, "addrs exceeds listLimit (max: 1)", func() { ListUsersByAddresses(regularUser, badUser) }) uassert.PanicsWithMessage(t, cur, "count exceeds listLimit (max: 1)", func() { ListUsers(1, 2) }) uassert.AbortsWithMessage(t, cur, "limit must be at least 1", func() { SetListLimit(cross(cur), 0) }) uassert.AbortsWithMessage(t, cur, "limit must be at least 1", func() { SetListLimit(cross(cur), -1) }) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewUserRealm(regularUser)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetListLimit(cross(cur), 50) }) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/akkadia/beta/user_admin_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetListLimit(cross(cur), 50) }) } func TestSelfServiceFrozen(cur realm, t *testing.T) { uassert.True(t, isFrozen) setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AdminUpdateUser(cross(cur), regularUser, "nickname", "Alice") before := ListUsersByAddresses(regularUser) testing.SetOriginCaller(anotherUser) testing.SetRealm(testing.NewUserRealm(anotherUser)) uassert.AbortsWithMessage(t, cur, "user self-service is frozen", func() { CreateUser(cross(cur)) }) uassert.False(t, HasUser(anotherUser)) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewUserRealm(regularUser)) uassert.AbortsWithMessage(t, cur, "user self-service is frozen", func() { DeleteUser(cross(cur)) }) uassert.Equal(t, before[0][createdAtKey], ListUsersByAddresses(regularUser)[0][createdAtKey]) uassert.Equal(t, "Alice", ListUsersByAddresses(regularUser)[0]["nickname"]) uassert.Equal(t, 1, GetTotalUserSize()) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) SetFrozen(cross(cur), false) testing.SetOriginCaller(anotherUser) testing.SetRealm(testing.NewUserRealm(anotherUser)) CreateUser(cross(cur)) uassert.True(t, HasUser(anotherUser)) DeleteUser(cross(cur)) uassert.False(t, HasUser(anotherUser)) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) SetFrozen(cross(cur), true) testing.SetOriginCaller(anotherUser) testing.SetRealm(testing.NewUserRealm(anotherUser)) uassert.AbortsWithMessage(t, cur, "user self-service is frozen", func() { CreateUser(cross(cur)) }) testing.SetOriginCaller(regularUser) testing.SetRealm(testing.NewUserRealm(regularUser)) uassert.AbortsWithMessage(t, cur, "user self-service is frozen", func() { DeleteUser(cross(cur)) }) uassert.Equal(t, before[0][createdAtKey], ListUsersByAddresses(regularUser)[0][createdAtKey]) uassert.Equal(t, "Alice", ListUsersByAddresses(regularUser)[0]["nickname"]) uassert.Equal(t, 1, GetTotalUserSize()) } func TestSetFrozenRejectsUnauthorized(cur realm, t *testing.T) { setupUserTests(cross(cur), t) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.SetOperator(cross(cur), anotherUser) for _, frozen := range []bool{true, false} { testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) SetFrozen(cross(cur), frozen) for _, caller := range []address{regularUser, anotherUser} { testing.SetOriginCaller(caller) testing.SetRealm(testing.NewUserRealm(caller)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetFrozen(cross(cur), !frozen) }) uassert.Equal(t, frozen, isFrozen) } testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/akkadia/beta/user_admin_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetFrozen(cross(cur), !frozen) }) uassert.Equal(t, frozen, isFrozen) } testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.UnsetOperator(cross(cur), anotherUser) SetFrozen(cross(cur), true) } func TestDeleteAll(cur realm, t *testing.T) { for _, frozen := range []bool{true, false} { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) createUserForTest(cross(cur), anotherUser) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AdminUpdateUser(cross(cur), regularUser, "nickname", "Alice") SetListLimit(cross(cur), 7) SetFrozen(cross(cur), frozen) DeleteAll(cross(cur)) uassert.Equal(t, 0, GetTotalUserSize()) uassert.False(t, HasUser(regularUser)) uassert.False(t, HasUser(anotherUser)) uassert.Equal(t, 0, len(ListUsers(1, 7))) uassert.Equal(t, 0, len(ListUsersByAddresses(regularUser, anotherUser))) uassert.Equal(t, 7, GetListLimit()) uassert.Equal(t, frozen, isFrozen) // Empty resets are safe, and re-registration must not restore old properties. DeleteAll(cross(cur)) AdminCreateUser(cross(cur), regularUser) uassert.Equal(t, 1, GetTotalUserSize()) uassert.True(t, HasUser(regularUser)) props := ListUsersByAddresses(regularUser)[0] uassert.Equal(t, "", props["nickname"]) uassert.True(t, props[createdAtKey] != "") } } func TestDeleteAllAuthority(cur realm, t *testing.T) { setupUserTests(cross(cur), t) createUserForTest(cross(cur), regularUser) testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) AdminUpdateUser(cross(cur), regularUser, "nickname", "Alice") admin.SetOperator(cross(cur), anotherUser) for _, caller := range []address{regularUser, anotherUser} { testing.SetOriginCaller(caller) testing.SetRealm(testing.NewUserRealm(caller)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { DeleteAll(cross(cur)) }) uassert.Equal(t, 1, GetTotalUserSize()) uassert.Equal(t, "Alice", ListUsersByAddresses(regularUser)[0]["nickname"]) } testing.SetOriginCaller(testAdmin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/akkadia/beta/user_admin_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { DeleteAll(cross(cur)) }) uassert.Equal(t, 1, GetTotalUserSize()) uassert.Equal(t, "Alice", ListUsersByAddresses(regularUser)[0]["nickname"]) testing.SetRealm(testing.NewUserRealm(testAdmin)) admin.UnsetOperator(cross(cur), anotherUser) }
- #14validation.gno
- #15package user import "unicode/utf8" const renderURLMaxRunes = 1024 func assertRenderURLLen(value string) { if utf8.RuneCountInString(value) > renderURLMaxRunes { panic("url exceeds max runes") } }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]