Skip to content

Commit fb40a63

Browse files
committed
feat: implement GetAllRolesMap and GetAllPermissionsMap, add row-count validation for creations, and add comprehensive test coverage.
1 parent f090cfa commit fb40a63

8 files changed

Lines changed: 884 additions & 2 deletions

File tree

edge_cases_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package gogate
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"testing"
7+
8+
"github.com/DATA-DOG/go-sqlmock"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestEdgeCases(t *testing.T) {
13+
db, mock, err := sqlmock.New()
14+
assert.NoError(t, err)
15+
defer db.Close()
16+
17+
gate := NewGate(db, nil)
18+
ctx := context.Background()
19+
20+
t.Run("IsNilOrEmpty", func(t *testing.T) {
21+
assert.True(t, IsNilOrEmpty(nil))
22+
assert.True(t, IsNilOrEmpty(""))
23+
assert.False(t, IsNilOrEmpty("not empty"))
24+
25+
var strPtr *string
26+
assert.True(t, IsNilOrEmpty(strPtr))
27+
28+
str := "val"
29+
assert.False(t, IsNilOrEmpty(&str))
30+
31+
var emptyUUID [16]byte
32+
assert.True(t, IsNilOrEmpty(emptyUUID))
33+
34+
validUUID := [16]byte{1}
35+
assert.False(t, IsNilOrEmpty(validUUID))
36+
37+
assert.False(t, IsNilOrEmpty(123)) // integer is not checked for empty in the func, so it defaults to false
38+
})
39+
40+
user := gate.Model("users", "1", nil)
41+
42+
t.Run("RemoveRole_ErrNoRows", func(t *testing.T) {
43+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnError(sql.ErrNoRows)
44+
err := user.RemoveRole(ctx, "nonexistent", "web")
45+
assert.NoError(t, err) // Should ignore ErrNoRows
46+
})
47+
48+
t.Run("RevokePermissionTo_ErrNoRows", func(t *testing.T) {
49+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnError(sql.ErrNoRows)
50+
err := user.RevokePermissionTo(ctx, "nonexistent", "web")
51+
assert.NoError(t, err) // Should ignore ErrNoRows
52+
})
53+
54+
role := gate.Role("admin", "web")
55+
56+
t.Run("Role_RevokePermissionTo_ErrNoRows", func(t *testing.T) {
57+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
58+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnError(sql.ErrNoRows)
59+
err := role.RevokePermissionTo(ctx, "nonexistent")
60+
assert.NoError(t, err) // Should ignore ErrNoRows
61+
})
62+
63+
t.Run("Role_RevokePermissionTo_RoleErrNoRows", func(t *testing.T) {
64+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnError(sql.ErrNoRows)
65+
err := role.RevokePermissionTo(ctx, "nonexistent")
66+
assert.NoError(t, err) // Should ignore ErrNoRows
67+
})
68+
69+
t.Run("Role_GivePermissionTo_ErrNoRows", func(t *testing.T) {
70+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnError(sql.ErrNoRows)
71+
err := role.GivePermissionTo(ctx, "nonexistent")
72+
assert.Error(t, err)
73+
74+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
75+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnError(sql.ErrNoRows)
76+
err = role.GivePermissionTo(ctx, "nonexistent")
77+
assert.Error(t, err)
78+
})
79+
80+
t.Run("HasAnyRole_Empty", func(t *testing.T) {
81+
ok, err := user.HasAnyRole(ctx, "web")
82+
assert.NoError(t, err)
83+
assert.False(t, ok)
84+
})
85+
86+
t.Run("HasAllRoles_Empty", func(t *testing.T) {
87+
ok, err := user.HasAllRoles(ctx, "web")
88+
assert.NoError(t, err)
89+
assert.True(t, ok)
90+
})
91+
92+
t.Run("HasAnyPermission_Empty", func(t *testing.T) {
93+
ok, err := user.HasAnyPermission(ctx)
94+
assert.NoError(t, err)
95+
assert.False(t, ok)
96+
})
97+
98+
t.Run("HasAllPermissions_Empty", func(t *testing.T) {
99+
ok, err := user.HasAllPermissions(ctx)
100+
assert.NoError(t, err)
101+
assert.True(t, ok)
102+
})
103+
104+
t.Run("Role_GetPermissionNames", func(t *testing.T) {
105+
// Mock memory state
106+
gate.mu.Lock()
107+
gate.rolePermissions = map[string]map[string]bool{
108+
"web:admin": {
109+
"edit": true,
110+
"delete": true,
111+
},
112+
}
113+
gate.mu.Unlock()
114+
115+
perms, err := role.GetPermissionNames(ctx)
116+
assert.NoError(t, err)
117+
assert.Contains(t, perms, "edit")
118+
assert.Contains(t, perms, "delete")
119+
assert.Len(t, perms, 2)
120+
})
121+
}

error_paths_test.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package gogate
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"testing"
7+
8+
"github.com/DATA-DOG/go-sqlmock"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestGate_ErrorPaths(t *testing.T) {
13+
db, mock, err := sqlmock.New()
14+
assert.NoError(t, err)
15+
defer db.Close()
16+
17+
gate := NewGate(db, nil)
18+
ctx := context.Background()
19+
20+
t.Run("LoadPolicy_DBError", func(t *testing.T) {
21+
mock.ExpectQuery(`SELECT r.guard_name`).WillReturnError(sql.ErrConnDone)
22+
err := gate.LoadPolicy(ctx)
23+
assert.Error(t, err)
24+
})
25+
26+
t.Run("Check_DBError", func(t *testing.T) {
27+
mock.ExpectQuery(`SELECT 'role' AS type`).WillReturnError(sql.ErrConnDone)
28+
_, err := gate.Check(ctx, "users", "1", "edit", "web", nil)
29+
assert.Error(t, err)
30+
})
31+
32+
user := gate.Model("users", "1", nil)
33+
34+
t.Run("AssignRole_DBError", func(t *testing.T) {
35+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnError(sql.ErrConnDone)
36+
err := user.AssignRole(ctx, "admin", "web")
37+
assert.Error(t, err)
38+
39+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
40+
mock.ExpectExec(`INSERT INTO model_has_roles`).WillReturnError(sql.ErrConnDone)
41+
err = user.AssignRole(ctx, "admin", "web")
42+
assert.Error(t, err)
43+
})
44+
45+
t.Run("RemoveRole_DBError", func(t *testing.T) {
46+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnError(sql.ErrConnDone)
47+
err := user.RemoveRole(ctx, "admin", "web")
48+
assert.Error(t, err)
49+
50+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
51+
mock.ExpectExec(`DELETE FROM model_has_roles`).WillReturnError(sql.ErrConnDone)
52+
err = user.RemoveRole(ctx, "admin", "web")
53+
assert.Error(t, err)
54+
})
55+
56+
t.Run("GivePermissionTo_DBError", func(t *testing.T) {
57+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnError(sql.ErrConnDone)
58+
err := user.GivePermissionTo(ctx, "edit", "web")
59+
assert.Error(t, err)
60+
61+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
62+
mock.ExpectExec(`INSERT INTO model_has_permissions`).WillReturnError(sql.ErrConnDone)
63+
err = user.GivePermissionTo(ctx, "edit", "web")
64+
assert.Error(t, err)
65+
})
66+
67+
t.Run("RevokePermissionTo_DBError", func(t *testing.T) {
68+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnError(sql.ErrConnDone)
69+
err := user.RevokePermissionTo(ctx, "edit", "web")
70+
assert.Error(t, err)
71+
72+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
73+
mock.ExpectExec(`DELETE FROM model_has_permissions`).WillReturnError(sql.ErrConnDone)
74+
err = user.RevokePermissionTo(ctx, "edit", "web")
75+
assert.Error(t, err)
76+
})
77+
78+
t.Run("GetRoleNames_DBError", func(t *testing.T) {
79+
mock.ExpectQuery(`SELECT r.name FROM model_has_roles`).WillReturnError(sql.ErrConnDone)
80+
_, err := user.GetRoleNames(ctx)
81+
assert.Error(t, err)
82+
})
83+
84+
t.Run("GetRolesMap_DBError", func(t *testing.T) {
85+
mock.ExpectQuery(`SELECT r.guard_name, r.name FROM model_has_roles`).WillReturnError(sql.ErrConnDone)
86+
_, err := user.GetRolesMap(ctx)
87+
assert.Error(t, err)
88+
})
89+
90+
t.Run("GetDirectPermissions_DBError", func(t *testing.T) {
91+
mock.ExpectQuery(`SELECT p.name FROM model_has_permissions`).WillReturnError(sql.ErrConnDone)
92+
_, err := user.GetDirectPermissions(ctx)
93+
assert.Error(t, err)
94+
})
95+
96+
t.Run("GetPermissionsViaRoles_DBError", func(t *testing.T) {
97+
mock.ExpectQuery(`SELECT r.name FROM model_has_roles`).WillReturnError(sql.ErrConnDone)
98+
_, err := user.GetPermissionsViaRoles(ctx)
99+
assert.Error(t, err)
100+
})
101+
102+
t.Run("GetAllPermissions_DBError1", func(t *testing.T) {
103+
mock.ExpectQuery(`SELECT p.name FROM model_has_permissions`).WillReturnError(sql.ErrConnDone)
104+
_, err := user.GetAllPermissions(ctx)
105+
assert.Error(t, err)
106+
})
107+
108+
t.Run("GetAllPermissions_DBError2", func(t *testing.T) {
109+
mock.ExpectQuery(`SELECT p.name FROM model_has_permissions`).WillReturnRows(sqlmock.NewRows([]string{"name"}))
110+
mock.ExpectQuery(`SELECT r.name FROM model_has_roles`).WillReturnError(sql.ErrConnDone)
111+
_, err := user.GetAllPermissions(ctx)
112+
assert.Error(t, err)
113+
})
114+
115+
t.Run("HasRole_DBError", func(t *testing.T) {
116+
mock.ExpectQuery(`SELECT EXISTS`).WillReturnError(sql.ErrConnDone)
117+
_, err := user.HasRole(ctx, "admin", "web")
118+
assert.Error(t, err)
119+
})
120+
121+
t.Run("HasAnyRole_DBError", func(t *testing.T) {
122+
mock.ExpectQuery(`SELECT r.name, r.guard_name FROM model_has_roles`).WillReturnError(sql.ErrConnDone)
123+
_, err := user.HasAnyRole(ctx, "web", "admin")
124+
assert.Error(t, err)
125+
})
126+
127+
t.Run("HasAllRoles_DBError", func(t *testing.T) {
128+
mock.ExpectQuery(`SELECT r.name, r.guard_name FROM model_has_roles`).WillReturnError(sql.ErrConnDone)
129+
_, err := user.HasAllRoles(ctx, "web", "admin")
130+
assert.Error(t, err)
131+
})
132+
133+
t.Run("HasAnyPermission_DBError", func(t *testing.T) {
134+
mock.ExpectQuery(`SELECT p.name FROM model_has_permissions`).WillReturnError(sql.ErrConnDone)
135+
_, err := user.HasAnyPermission(ctx, "edit")
136+
assert.Error(t, err)
137+
})
138+
139+
t.Run("HasAllPermissions_DBError", func(t *testing.T) {
140+
mock.ExpectQuery(`SELECT p.name FROM model_has_permissions`).WillReturnError(sql.ErrConnDone)
141+
_, err := user.HasAllPermissions(ctx, "edit")
142+
assert.Error(t, err)
143+
})
144+
145+
t.Run("Model_Can", func(t *testing.T) {
146+
mock.ExpectQuery(`SELECT 'role' AS type`).WillReturnError(sql.ErrConnDone)
147+
_, err := user.Can(ctx, "edit", "web")
148+
assert.Error(t, err)
149+
})
150+
151+
role := gate.Role("admin", "web")
152+
153+
t.Run("Role_GivePermissionTo_DBError", func(t *testing.T) {
154+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnError(sql.ErrConnDone)
155+
err := role.GivePermissionTo(ctx, "edit")
156+
assert.Error(t, err)
157+
158+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
159+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnError(sql.ErrConnDone)
160+
err = role.GivePermissionTo(ctx, "edit")
161+
assert.Error(t, err)
162+
163+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
164+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
165+
mock.ExpectExec(`INSERT INTO role_has_permissions`).WillReturnError(sql.ErrConnDone)
166+
err = role.GivePermissionTo(ctx, "edit")
167+
assert.Error(t, err)
168+
})
169+
170+
t.Run("Role_RevokePermissionTo_DBError", func(t *testing.T) {
171+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnError(sql.ErrConnDone)
172+
err := role.RevokePermissionTo(ctx, "edit")
173+
assert.Error(t, err)
174+
175+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
176+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnError(sql.ErrConnDone)
177+
err = role.RevokePermissionTo(ctx, "edit")
178+
assert.Error(t, err)
179+
180+
mock.ExpectQuery(`SELECT id FROM roles`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
181+
mock.ExpectQuery(`SELECT id FROM permissions`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
182+
mock.ExpectExec(`DELETE FROM role_has_permissions`).WillReturnError(sql.ErrConnDone)
183+
err = role.RevokePermissionTo(ctx, "edit")
184+
assert.Error(t, err)
185+
})
186+
187+
t.Run("GetRoleNames_RowScanError", func(t *testing.T) {
188+
mock.ExpectQuery(`SELECT r.name FROM model_has_roles`).
189+
WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("admin").RowError(0, sql.ErrConnDone))
190+
_, err := user.GetRoleNames(ctx)
191+
assert.Error(t, err)
192+
})
193+
194+
t.Run("GetRolesMap_RowScanError", func(t *testing.T) {
195+
mock.ExpectQuery(`SELECT r.guard_name, r.name FROM model_has_roles`).
196+
WillReturnRows(sqlmock.NewRows([]string{"name", "guard_name"}).AddRow("admin", "web").RowError(0, sql.ErrConnDone))
197+
_, err := user.GetRolesMap(ctx)
198+
assert.Error(t, err)
199+
})
200+
201+
t.Run("GetDirectPermissions_RowScanError", func(t *testing.T) {
202+
mock.ExpectQuery(`SELECT p.name FROM model_has_permissions`).
203+
WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("edit").RowError(0, sql.ErrConnDone))
204+
_, err := user.GetDirectPermissions(ctx)
205+
assert.Error(t, err)
206+
})
207+
}

gate.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ package gogate
33
import (
44
"context"
55
"database/sql"
6+
"errors"
67
"fmt"
78
"sync"
89
)
910

11+
var (
12+
ErrRoleAlreadyExists = errors.New("wpd-gogate: role already exists")
13+
ErrPermissionAlreadyExists = errors.New("wpd-gogate: permission already exists")
14+
)
15+
1016
// DBTX is the minimal database interface required by wpd-gogate.
1117
// It is satisfied by *sql.DB and *sql.Tx.
1218
type DBTX interface {

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ require (
66
github.com/DATA-DOG/go-sqlmock v1.5.2
77
github.com/labstack/echo/v4 v4.15.2
88
github.com/lib/pq v1.12.3
9+
github.com/stretchr/testify v1.11.1
910
)
1011

1112
require (
13+
github.com/davecgh/go-spew v1.1.1 // indirect
1214
github.com/labstack/gommon v0.5.0 // indirect
1315
github.com/mattn/go-colorable v0.1.15 // indirect
1416
github.com/mattn/go-isatty v0.0.22 // indirect
17+
github.com/pmezard/go-difflib v1.0.0 // indirect
1518
github.com/valyala/bytebufferpool v1.0.0 // indirect
1619
github.com/valyala/fasttemplate v1.2.2 // indirect
1720
golang.org/x/crypto v0.53.0 // indirect
1821
golang.org/x/net v0.55.0 // indirect
1922
golang.org/x/sys v0.46.0 // indirect
2023
golang.org/x/text v0.38.0 // indirect
24+
gopkg.in/yaml.v3 v3.0.1 // indirect
2125
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,7 @@ golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
2929
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
3030
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
3131
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
32+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
33+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3234
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
3335
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)