@budibase/worker 2.3.17-alpha.3 → 2.3.17-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/scripts/dev/manage.js +1 -0
- package/src/api/controllers/global/auth.ts +77 -70
- package/src/api/controllers/global/self.ts +28 -44
- package/src/api/controllers/global/users.ts +65 -25
- package/src/api/controllers/system/accounts.ts +7 -5
- package/src/api/controllers/system/tenants.ts +4 -8
- package/src/api/index.ts +4 -20
- package/src/api/routes/global/auth.ts +10 -7
- package/src/api/routes/global/tests/auth.spec.ts +234 -33
- package/src/api/routes/global/tests/configs.spec.ts +93 -113
- package/src/api/routes/global/tests/roles.spec.ts +3 -2
- package/src/api/routes/global/tests/self.spec.ts +3 -4
- package/src/api/routes/global/tests/users.spec.ts +44 -46
- package/src/api/routes/system/tenants.ts +1 -1
- package/src/api/routes/system/tests/accounts.spec.ts +4 -4
- package/src/api/routes/system/tests/migrations.spec.ts +2 -2
- package/src/api/routes/system/tests/restore.spec.ts +2 -2
- package/src/api/routes/system/tests/status.spec.ts +5 -5
- package/src/environment.ts +15 -1
- package/src/index.ts +6 -0
- package/src/middleware/tests/tenancy.spec.ts +4 -4
- package/src/migrations/functions/globalInfoSyncUsers.ts +4 -3
- package/src/sdk/accounts/index.ts +2 -1
- package/src/sdk/accounts/{accounts.ts → metadata.ts} +0 -1
- package/src/sdk/auth/auth.ts +86 -0
- package/src/sdk/auth/index.ts +1 -0
- package/src/sdk/tenants/index.ts +1 -0
- package/src/sdk/tenants/tenants.ts +76 -0
- package/src/sdk/users/events.ts +4 -0
- package/src/sdk/users/index.ts +1 -0
- package/src/sdk/users/tests/users.spec.ts +52 -0
- package/src/sdk/users/users.ts +53 -46
- package/src/tests/TestConfiguration.ts +38 -89
- package/src/tests/api/auth.ts +42 -18
- package/src/tests/api/base.ts +2 -1
- package/src/tests/api/restore.ts +1 -0
- package/src/tests/jestEnv.ts +2 -1
- package/src/tests/jestSetup.ts +2 -5
- package/src/tests/logging.ts +34 -0
- package/src/tests/structures/index.ts +0 -2
- package/src/utilities/email.ts +3 -3
- package/src/tests/structures/users.ts +0 -37
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { Account, AccountMetadata } from "@budibase/types"
|
|
2
|
-
import
|
|
2
|
+
import * as accounts from "../../../sdk/accounts"
|
|
3
3
|
|
|
4
4
|
export const save = async (ctx: any) => {
|
|
5
5
|
const account = ctx.request.body as Account
|
|
6
6
|
let metadata: AccountMetadata = {
|
|
7
|
-
_id:
|
|
7
|
+
_id: accounts.metadata.formatAccountMetadataId(account.accountId),
|
|
8
8
|
email: account.email,
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
metadata = await
|
|
11
|
+
metadata = await accounts.metadata.saveMetadata(metadata)
|
|
12
12
|
|
|
13
13
|
ctx.body = metadata
|
|
14
14
|
ctx.status = 200
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export const destroy = async (ctx: any) => {
|
|
18
|
-
const accountId =
|
|
19
|
-
|
|
18
|
+
const accountId = accounts.metadata.formatAccountMetadataId(
|
|
19
|
+
ctx.params.accountId
|
|
20
|
+
)
|
|
21
|
+
await accounts.metadata.destroyMetadata(accountId)
|
|
20
22
|
ctx.status = 204
|
|
21
23
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { quotas } from "@budibase/pro"
|
|
1
|
+
import { UserCtx } from "@budibase/types"
|
|
2
|
+
import * as tenantSdk from "../../../sdk/tenants"
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
export async function destroy(ctx: UserCtx) {
|
|
6
5
|
const user = ctx.user!
|
|
7
6
|
const tenantId = ctx.params.tenantId
|
|
8
7
|
|
|
@@ -11,13 +10,10 @@ const _delete = async (ctx: BBContext) => {
|
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
try {
|
|
14
|
-
await
|
|
15
|
-
await deprovisioning.deleteTenant(tenantId)
|
|
13
|
+
await tenantSdk.deleteTenant(tenantId)
|
|
16
14
|
ctx.status = 204
|
|
17
15
|
} catch (err) {
|
|
18
16
|
ctx.log.error(err)
|
|
19
17
|
throw err
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
|
-
|
|
23
|
-
export { _delete as delete }
|
package/src/api/index.ts
CHANGED
|
@@ -3,8 +3,7 @@ const compress = require("koa-compress")
|
|
|
3
3
|
const zlib = require("zlib")
|
|
4
4
|
import { routes } from "./routes"
|
|
5
5
|
import { middleware as pro } from "@budibase/pro"
|
|
6
|
-
import {
|
|
7
|
-
import { APIError } from "@budibase/types"
|
|
6
|
+
import { auth, middleware } from "@budibase/backend-core"
|
|
8
7
|
|
|
9
8
|
const PUBLIC_ENDPOINTS = [
|
|
10
9
|
// deprecated single tenant sso callback
|
|
@@ -109,7 +108,9 @@ const NO_TENANCY_ENDPOINTS = [
|
|
|
109
108
|
const NO_CSRF_ENDPOINTS = [...PUBLIC_ENDPOINTS]
|
|
110
109
|
|
|
111
110
|
const router: Router = new Router()
|
|
111
|
+
|
|
112
112
|
router
|
|
113
|
+
.use(middleware.errorHandling)
|
|
113
114
|
.use(
|
|
114
115
|
compress({
|
|
115
116
|
threshold: 2048,
|
|
@@ -136,29 +137,12 @@ router
|
|
|
136
137
|
(!ctx.isAuthenticated || (ctx.user && !ctx.user.budibaseAccess)) &&
|
|
137
138
|
!ctx.internal
|
|
138
139
|
) {
|
|
139
|
-
ctx.throw(403, "Unauthorized
|
|
140
|
+
ctx.throw(403, "Unauthorized")
|
|
140
141
|
}
|
|
141
142
|
return next()
|
|
142
143
|
})
|
|
143
144
|
.use(middleware.auditLog)
|
|
144
145
|
|
|
145
|
-
// error handling middleware - TODO: This could be moved to backend-core
|
|
146
|
-
router.use(async (ctx, next) => {
|
|
147
|
-
try {
|
|
148
|
-
await next()
|
|
149
|
-
} catch (err: any) {
|
|
150
|
-
ctx.log.error(err)
|
|
151
|
-
ctx.status = err.status || err.statusCode || 500
|
|
152
|
-
const error = errors.getPublicError(err)
|
|
153
|
-
const body: APIError = {
|
|
154
|
-
message: err.message,
|
|
155
|
-
status: ctx.status,
|
|
156
|
-
error,
|
|
157
|
-
}
|
|
158
|
-
ctx.body = body
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
146
|
router.get("/health", ctx => (ctx.status = 200))
|
|
163
147
|
|
|
164
148
|
// authenticated routes
|
|
@@ -33,7 +33,7 @@ router
|
|
|
33
33
|
.post(
|
|
34
34
|
"/api/global/auth/:tenantId/login",
|
|
35
35
|
buildAuthValidation(),
|
|
36
|
-
authController.
|
|
36
|
+
authController.login
|
|
37
37
|
)
|
|
38
38
|
.post("/api/global/auth/logout", authController.logout)
|
|
39
39
|
.post(
|
|
@@ -68,21 +68,24 @@ router
|
|
|
68
68
|
|
|
69
69
|
// GOOGLE - MULTI TENANT
|
|
70
70
|
.get("/api/global/auth/:tenantId/google", authController.googlePreAuth)
|
|
71
|
-
.get(
|
|
71
|
+
.get(
|
|
72
|
+
"/api/global/auth/:tenantId/google/callback",
|
|
73
|
+
authController.googleCallback
|
|
74
|
+
)
|
|
72
75
|
|
|
73
76
|
// GOOGLE - SINGLE TENANT - DEPRECATED
|
|
74
|
-
.get("/api/global/auth/google/callback", authController.
|
|
75
|
-
.get("/api/admin/auth/google/callback", authController.
|
|
77
|
+
.get("/api/global/auth/google/callback", authController.googleCallback)
|
|
78
|
+
.get("/api/admin/auth/google/callback", authController.googleCallback)
|
|
76
79
|
|
|
77
80
|
// OIDC - MULTI TENANT
|
|
78
81
|
.get(
|
|
79
82
|
"/api/global/auth/:tenantId/oidc/configs/:configId",
|
|
80
83
|
authController.oidcPreAuth
|
|
81
84
|
)
|
|
82
|
-
.get("/api/global/auth/:tenantId/oidc/callback", authController.
|
|
85
|
+
.get("/api/global/auth/:tenantId/oidc/callback", authController.oidcCallback)
|
|
83
86
|
|
|
84
87
|
// OIDC - SINGLE TENANT - DEPRECATED
|
|
85
|
-
.get("/api/global/auth/oidc/callback", authController.
|
|
86
|
-
.get("/api/admin/auth/oidc/callback", authController.
|
|
88
|
+
.get("/api/global/auth/oidc/callback", authController.oidcCallback)
|
|
89
|
+
.get("/api/admin/auth/oidc/callback", authController.oidcCallback)
|
|
87
90
|
|
|
88
91
|
export default router
|
|
@@ -1,13 +1,27 @@
|
|
|
1
|
+
import { CloudAccount, SSOUser, User } from "@budibase/types"
|
|
2
|
+
|
|
1
3
|
jest.mock("nodemailer")
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
TestConfiguration,
|
|
6
|
+
mocks,
|
|
7
|
+
structures,
|
|
8
|
+
generator,
|
|
9
|
+
} from "../../../../tests"
|
|
3
10
|
const sendMailMock = mocks.email.mock()
|
|
4
|
-
import { events,
|
|
5
|
-
import {
|
|
11
|
+
import { events, constants } from "@budibase/backend-core"
|
|
12
|
+
import { Response } from "superagent"
|
|
13
|
+
|
|
14
|
+
import * as userSdk from "../../../../sdk/users"
|
|
15
|
+
|
|
16
|
+
function getAuthCookie(response: Response) {
|
|
17
|
+
return response.headers["set-cookie"]
|
|
18
|
+
.find((s: string) => s.startsWith(`${constants.Cookie.Auth}=`))
|
|
19
|
+
.split("=")[1]
|
|
20
|
+
.split(";")[0]
|
|
21
|
+
}
|
|
6
22
|
|
|
7
|
-
const expectSetAuthCookie = (
|
|
8
|
-
expect(
|
|
9
|
-
res.get("Set-Cookie").find((c: string) => c.startsWith("budibase:auth"))
|
|
10
|
-
).toBeDefined()
|
|
23
|
+
const expectSetAuthCookie = (response: Response) => {
|
|
24
|
+
expect(getAuthCookie(response).length > 1).toBe(true)
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
describe("/api/global/auth", () => {
|
|
@@ -25,60 +39,247 @@ describe("/api/global/auth", () => {
|
|
|
25
39
|
jest.clearAllMocks()
|
|
26
40
|
})
|
|
27
41
|
|
|
42
|
+
async function createSSOUser() {
|
|
43
|
+
return config.doInTenant(async () => {
|
|
44
|
+
return userSdk.save(structures.users.ssoUser(), {
|
|
45
|
+
requirePassword: false,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
describe("password", () => {
|
|
29
51
|
describe("POST /api/global/auth/:tenantId/login", () => {
|
|
30
|
-
it("
|
|
52
|
+
it("logs in with correct credentials", async () => {
|
|
53
|
+
const tenantId = config.tenantId!
|
|
54
|
+
const email = config.user?.email!
|
|
55
|
+
const password = config.userPassword
|
|
56
|
+
|
|
57
|
+
const response = await config.api.auth.login(tenantId, email, password)
|
|
58
|
+
|
|
59
|
+
expectSetAuthCookie(response)
|
|
60
|
+
expect(events.auth.login).toBeCalledTimes(1)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("should return 403 with incorrect credentials", async () => {
|
|
64
|
+
const tenantId = config.tenantId!
|
|
65
|
+
const email = config.user?.email!
|
|
66
|
+
const password = "incorrect"
|
|
67
|
+
|
|
68
|
+
const response = await config.api.auth.login(
|
|
69
|
+
tenantId,
|
|
70
|
+
email,
|
|
71
|
+
password,
|
|
72
|
+
{ status: 403 }
|
|
73
|
+
)
|
|
74
|
+
expect(response.body).toEqual({
|
|
75
|
+
message: "Invalid credentials",
|
|
76
|
+
status: 403,
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("should return 403 when user doesn't exist", async () => {
|
|
81
|
+
const tenantId = config.tenantId!
|
|
82
|
+
const email = "invaliduser@test.com"
|
|
83
|
+
const password = "password"
|
|
84
|
+
|
|
85
|
+
const response = await config.api.auth.login(
|
|
86
|
+
tenantId,
|
|
87
|
+
email,
|
|
88
|
+
password,
|
|
89
|
+
{ status: 403 }
|
|
90
|
+
)
|
|
91
|
+
expect(response.body).toEqual({
|
|
92
|
+
message: "Invalid credentials",
|
|
93
|
+
status: 403,
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe("sso user", () => {
|
|
98
|
+
let user: User
|
|
99
|
+
|
|
100
|
+
async function testSSOUser() {
|
|
101
|
+
const tenantId = user.tenantId!
|
|
102
|
+
const email = user.email
|
|
103
|
+
const password = "test"
|
|
104
|
+
|
|
105
|
+
const response = await config.api.auth.login(
|
|
106
|
+
tenantId,
|
|
107
|
+
email,
|
|
108
|
+
password,
|
|
109
|
+
{ status: 400 }
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
expect(response.body).toEqual({
|
|
113
|
+
message: "SSO user cannot login using password",
|
|
114
|
+
status: 400,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe("budibase sso user", () => {
|
|
119
|
+
it("should prevent user from logging in", async () => {
|
|
120
|
+
user = await createSSOUser()
|
|
121
|
+
await testSSOUser()
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe("root account sso user", () => {
|
|
126
|
+
it("should prevent user from logging in", async () => {
|
|
127
|
+
user = await config.createUser()
|
|
128
|
+
const account = structures.accounts.ssoAccount() as CloudAccount
|
|
129
|
+
mocks.accounts.getAccount.mockReturnValueOnce(
|
|
130
|
+
Promise.resolve(account)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
await testSSOUser()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
31
137
|
})
|
|
32
138
|
|
|
33
139
|
describe("POST /api/global/auth/logout", () => {
|
|
34
140
|
it("should logout", async () => {
|
|
35
|
-
await config.api.auth.logout()
|
|
141
|
+
const response = await config.api.auth.logout()
|
|
36
142
|
expect(events.auth.logout).toBeCalledTimes(1)
|
|
37
143
|
|
|
38
|
-
|
|
144
|
+
const authCookie = getAuthCookie(response)
|
|
145
|
+
expect(authCookie).toBe("")
|
|
39
146
|
})
|
|
40
147
|
})
|
|
41
148
|
|
|
42
149
|
describe("POST /api/global/auth/:tenantId/reset", () => {
|
|
43
150
|
it("should generate password reset email", async () => {
|
|
44
|
-
await
|
|
45
|
-
|
|
46
|
-
|
|
151
|
+
const user = await config.createUser()
|
|
152
|
+
|
|
153
|
+
const { res, code } = await config.api.auth.requestPasswordReset(
|
|
154
|
+
sendMailMock,
|
|
155
|
+
user.email
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
expect(res.body).toEqual({
|
|
159
|
+
message: "Please check your email for a reset link.",
|
|
160
|
+
})
|
|
161
|
+
expect(sendMailMock).toHaveBeenCalled()
|
|
162
|
+
expect(code).toBeDefined()
|
|
163
|
+
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
|
|
164
|
+
expect(events.user.passwordResetRequested).toBeCalledWith(user)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe("sso user", () => {
|
|
168
|
+
let user: User
|
|
169
|
+
|
|
170
|
+
async function testSSOUser() {
|
|
171
|
+
const { res } = await config.api.auth.requestPasswordReset(
|
|
47
172
|
sendMailMock,
|
|
48
|
-
|
|
173
|
+
user.email,
|
|
174
|
+
{ status: 400 }
|
|
49
175
|
)
|
|
50
|
-
const user = await config.getUser(userEmail)
|
|
51
176
|
|
|
52
177
|
expect(res.body).toEqual({
|
|
53
|
-
message: "
|
|
178
|
+
message: "SSO user cannot reset password",
|
|
179
|
+
status: 400,
|
|
180
|
+
error: {
|
|
181
|
+
code: "http",
|
|
182
|
+
type: "generic",
|
|
183
|
+
},
|
|
54
184
|
})
|
|
55
|
-
expect(sendMailMock).toHaveBeenCalled()
|
|
185
|
+
expect(sendMailMock).not.toHaveBeenCalled()
|
|
186
|
+
}
|
|
56
187
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
188
|
+
describe("budibase sso user", () => {
|
|
189
|
+
it("should prevent user from generating password reset email", async () => {
|
|
190
|
+
user = await createSSOUser()
|
|
191
|
+
await testSSOUser()
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe("root account sso user", () => {
|
|
196
|
+
it("should prevent user from generating password reset email", async () => {
|
|
197
|
+
user = await config.createUser(structures.users.user())
|
|
198
|
+
const account = structures.accounts.ssoAccount() as CloudAccount
|
|
199
|
+
mocks.accounts.getAccount.mockReturnValueOnce(
|
|
200
|
+
Promise.resolve(account)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
await testSSOUser()
|
|
204
|
+
})
|
|
60
205
|
})
|
|
61
206
|
})
|
|
62
207
|
})
|
|
63
208
|
|
|
64
209
|
describe("POST /api/global/auth/:tenantId/reset/update", () => {
|
|
65
210
|
it("should reset password", async () => {
|
|
66
|
-
await
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
211
|
+
let user = await config.createUser()
|
|
212
|
+
const { code } = await config.api.auth.requestPasswordReset(
|
|
213
|
+
sendMailMock,
|
|
214
|
+
user.email
|
|
215
|
+
)
|
|
216
|
+
delete user.password
|
|
217
|
+
|
|
218
|
+
const newPassword = "newpassword"
|
|
219
|
+
const res = await config.api.auth.updatePassword(code!, newPassword)
|
|
220
|
+
|
|
221
|
+
user = await config.getUser(user.email)
|
|
222
|
+
delete user.password
|
|
223
|
+
|
|
224
|
+
expect(res.body).toEqual({ message: "password reset successfully." })
|
|
225
|
+
expect(events.user.passwordReset).toBeCalledTimes(1)
|
|
226
|
+
expect(events.user.passwordReset).toBeCalledWith(user)
|
|
227
|
+
|
|
228
|
+
// login using new password
|
|
229
|
+
await config.api.auth.login(user.tenantId, user.email, newPassword)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe("sso user", () => {
|
|
233
|
+
let user: User | SSOUser
|
|
234
|
+
|
|
235
|
+
async function testSSOUser(code: string) {
|
|
236
|
+
const res = await config.api.auth.updatePassword(
|
|
237
|
+
code!,
|
|
238
|
+
generator.string(),
|
|
239
|
+
{ status: 400 }
|
|
71
240
|
)
|
|
72
|
-
const user = await config.getUser(userEmail)
|
|
73
|
-
delete user.password
|
|
74
241
|
|
|
75
|
-
|
|
242
|
+
expect(res.body).toEqual({
|
|
243
|
+
message: "Cannot reset password.",
|
|
244
|
+
status: 400,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
describe("budibase sso user", () => {
|
|
249
|
+
it("should prevent user from generating password reset email", async () => {
|
|
250
|
+
user = await config.createUser()
|
|
251
|
+
const { code } = await config.api.auth.requestPasswordReset(
|
|
252
|
+
sendMailMock,
|
|
253
|
+
user.email
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// convert to sso now that password reset has been requested
|
|
257
|
+
const ssoUser = user as SSOUser
|
|
258
|
+
ssoUser.providerType = structures.sso.providerType()
|
|
259
|
+
delete ssoUser.password
|
|
260
|
+
await config.doInTenant(() => userSdk.save(ssoUser))
|
|
261
|
+
|
|
262
|
+
await testSSOUser(code!)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
76
265
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
266
|
+
describe("root account sso user", () => {
|
|
267
|
+
it("should prevent user from generating password reset email", async () => {
|
|
268
|
+
user = await config.createUser()
|
|
269
|
+
const { code } = await config.api.auth.requestPasswordReset(
|
|
270
|
+
sendMailMock,
|
|
271
|
+
user.email
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
// convert to account owner now that password has been requested
|
|
275
|
+
const account = structures.accounts.ssoAccount() as CloudAccount
|
|
276
|
+
mocks.accounts.getAccount.mockReturnValueOnce(
|
|
277
|
+
Promise.resolve(account)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
await testSSOUser(code!)
|
|
281
|
+
})
|
|
80
282
|
})
|
|
81
|
-
// TODO: Login using new password
|
|
82
283
|
})
|
|
83
284
|
})
|
|
84
285
|
})
|
|
@@ -153,7 +354,7 @@ describe("/api/global/auth", () => {
|
|
|
153
354
|
const location: string = res.get("location")
|
|
154
355
|
expect(
|
|
155
356
|
location.startsWith(
|
|
156
|
-
|
|
357
|
+
`http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2F${config.tenantId}%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access`
|
|
157
358
|
)
|
|
158
359
|
).toBe(true)
|
|
159
360
|
})
|