Skip to content

Commit

Permalink
feat: write tests for japa api clients
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jan 16, 2024
1 parent 59c6d78 commit d8a3a77
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 59 deletions.
4 changes: 2 additions & 2 deletions factories/access_tokens/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export class AccessTokensFakeUserProvider

async createToken(
user: AccessTokensFakeUser,
expiresIn?: string | number,
abilities?: string[]
abilities?: string[],
expiresIn?: string | number
): Promise<AccessToken> {
const transientToken = AccessToken.createTransientToken(user.id, 40, expiresIn)
const id = stringHelpers.random(15)
Expand Down
8 changes: 6 additions & 2 deletions factories/auth/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ export class FakeGuard implements GuardContract<FakeUser> {
}
}

async authenticateAsClient(_: FakeUser): Promise<AuthClientResponse> {
throw new Error('Not supported')
async authenticateAsClient(
_user: FakeUser,
_abilities?: string[],
_expiresIn?: string | number
): Promise<AuthClientResponse> {
return {}
}
}
11 changes: 9 additions & 2 deletions modules/access_tokens_guard/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,16 @@ export class AccessTokensGuard<UserProvider extends AccessTokensUserProviderCont
* the request.
*/
async authenticateAsClient(
_: UserProvider[typeof PROVIDER_REAL_USER]
user: UserProvider[typeof PROVIDER_REAL_USER],
abilities?: string[],
expiresIn?: string | number
): Promise<AuthClientResponse> {
throw new Error('Not supported')
const token = await this.#userProvider.createToken(user, abilities, expiresIn)
return {
headers: {
authorization: `Bearer ${token.value!.release()}`,
},
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion modules/access_tokens_guard/token_providers/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { RuntimeException } from '@adonisjs/core/exceptions'
* The user must be an instance of the associated user model.
*/
export class DbAccessTokensProvider<TokenableModel extends LucidModel>
implements AccessTokensProviderContract
implements AccessTokensProviderContract<TokenableModel>
{
/**
* Create tokens provider instance for a given Lucid model
Expand Down
22 changes: 20 additions & 2 deletions modules/access_tokens_guard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,16 @@ export type AccessTokenDbColumns = {
* Access token providers are used verify an access token
* during authentication
*/
export interface AccessTokensProviderContract {
export interface AccessTokensProviderContract<Tokenable extends LucidModel> {
/**
* Create a token for a given user
*/
create(
user: InstanceType<Tokenable>,
abilities?: string[],
expiresIn?: string | number
): Promise<AccessToken>

/**
* Verifies a publicly shared access token and returns an
* access token for it.
Expand All @@ -139,7 +148,7 @@ export interface AccessTokensProviderContract {
* authentication
*/
export type LucidTokenable<TokenableProperty extends string> = LucidModel & {
[K in TokenableProperty]: AccessTokensProviderContract
[K in TokenableProperty]: AccessTokensProviderContract<LucidModel>
}

/**
Expand Down Expand Up @@ -181,6 +190,15 @@ export interface AccessTokensUserProviderContract<RealUser> {
*/
createUserForGuard(user: RealUser): Promise<AccessTokensGuardUser<RealUser>>

/**
* Create a token for a given user
*/
createToken(
user: RealUser,
abilities?: string[],
expiresIn?: string | number
): Promise<AccessToken>

/**
* Find a user by their id.
*/
Expand Down
23 changes: 19 additions & 4 deletions modules/access_tokens_guard/user_providers/lucid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,6 @@ export class AccessTokensLucidUserProvider<
user: InstanceType<UserModel>
): Promise<AccessTokensGuardUser<InstanceType<UserModel>>> {
const model = await this.getModel()

/**
* Ensure user is an instance of the model
*/
if (user instanceof model === false) {
throw new RuntimeException(
`Invalid user object. It must be an instance of the "${model.name}" model`
Expand All @@ -106,6 +102,25 @@ export class AccessTokensLucidUserProvider<
}
}

/**
* Create a token for a given user
*/
async createToken(
user: InstanceType<UserModel>,
abilities?: string[] | undefined,
expiresIn?: string | number | undefined
): Promise<AccessToken> {
const model = await this.getModel()
if (user instanceof model === false) {
throw new RuntimeException(
`Invalid user object. It must be an instance of the "${model.name}" model`
)
}

const tokensProvider = await this.getTokensProvider()
return tokensProvider.create(user, abilities, expiresIn)
}

/**
* Finds a user by their primary key value
*/
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"@types/luxon": "^3.4.0",
"@types/node": "^20.10.8",
"@types/set-cookie-parser": "^2.4.7",
"@types/sinon": "^17.0.3",
"c8": "^9.0.0",
"convert-hrtime": "^5.0.0",
"copyfiles": "^2.4.1",
Expand All @@ -91,10 +92,12 @@
"github-label-sync": "^2.3.1",
"husky": "^8.0.3",
"luxon": "^3.4.4",
"nock": "^13.5.0",
"np": "^9.2.0",
"playwright": "^1.40.1",
"prettier": "^3.1.1",
"set-cookie-parser": "^2.6.0",
"sinon": "^17.0.1",
"sqlite3": "^5.1.7",
"timekeeper": "^2.3.1",
"ts-node": "^10.9.2",
Expand Down
18 changes: 5 additions & 13 deletions src/auth_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,30 @@ import { AuthenticatorClient } from './authenticator_client.js'
* guards from the config
*/
export class AuthManager<KnownGuards extends Record<string, GuardFactory>> {
/**
* Registered guards
*/
#config: {
default: keyof KnownGuards
guards: KnownGuards
}

/**
* Name of the default guard
*/
get defaultGuard() {
return this.#config.default
return this.config.default
}

constructor(config: { default: keyof KnownGuards; guards: KnownGuards }) {
this.#config = config
constructor(public config: { default: keyof KnownGuards; guards: KnownGuards }) {
this.config = config
}

/**
* Create an authenticator for a given HTTP request. The authenticator
* is used to authenticated in incoming HTTP request
*/
createAuthenticator(ctx: HttpContext) {
return new Authenticator<KnownGuards>(ctx, this.#config)
return new Authenticator<KnownGuards>(ctx, this.config)
}

/**
* Creates an instance of the authenticator client. The client is
* used to setup authentication state during testing.
*/
createAuthenticatorClient() {
return new AuthenticatorClient<KnownGuards>(this.#config)
return new AuthenticatorClient<KnownGuards>(this.config)
}
}
35 changes: 18 additions & 17 deletions src/plugins/japa/api_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,24 @@ import type { Authenticators, GuardContract, GuardFactory } from '../../types.js
declare module '@japa/api-client' {
export interface ApiRequest {
authData: {
guard: string
user: unknown
guard: keyof Authenticators | '__default__'
args: [unknown, ...any[]]
}

/**
* Login a user using the default authentication guard
* when making an API call
*/
loginAs(user: {
[K in keyof Authenticators]: Authenticators[K] extends GuardFactory
? ReturnType<Authenticators[K]> extends GuardContract<infer A>
? A
loginAs(
user: {
[K in keyof Authenticators]: Authenticators[K] extends GuardFactory
? ReturnType<Authenticators[K]> extends GuardContract<infer A>
? A
: never
: never
: never
}): this
}[keyof Authenticators],
...args: any[]
): this

/**
* Define the authentication guard for login
Expand All @@ -46,10 +49,8 @@ declare module '@japa/api-client' {
* Login a user using a specific auth guard
*/
loginAs(
user: Authenticators[K] extends GuardFactory
? ReturnType<Authenticators[K]> extends GuardContract<infer A>
? A
: never
...args: ReturnType<Authenticators[K]> extends GuardContract<any>
? Parameters<ReturnType<Authenticators[K]>['authenticateAsClient']>
: never
): Self
}
Expand All @@ -68,10 +69,10 @@ export const authApiClient = (app: ApplicationService) => {
* Login a user using the default authentication guard
* when making an API call
*/
ApiRequest.macro('loginAs', function (this: ApiRequest, user) {
ApiRequest.macro('loginAs', function (this: ApiRequest, user, ...args: any[]) {
this.authData = {
guard: '__default__',
user: user,
args: [user, ...args],
}
return this
})
Expand All @@ -84,10 +85,10 @@ export const authApiClient = (app: ApplicationService) => {
Self extends ApiRequest,
>(this: Self, guard: K) {
return {
loginAs: (user) => {
loginAs: (...args) => {
this.authData = {
guard,
user: user,
args: args,
}
return this
},
Expand All @@ -107,7 +108,7 @@ export const authApiClient = (app: ApplicationService) => {
const client = auth.createAuthenticatorClient()
const guard = authData.guard === '__default__' ? client.use() : client.use(authData.guard)
const requestData = await (guard as GuardContract<unknown>).authenticateAsClient(
authData.user
...authData.args
)

if (requestData.headers) {
Expand Down
33 changes: 20 additions & 13 deletions src/plugins/japa/browser_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ declare module 'playwright' {
* Login a user using the default authentication guard when
* using the browser context to make page visits
*/
loginAs(user: {
[K in keyof Authenticators]: Authenticators[K] extends GuardFactory
? ReturnType<Authenticators[K]> extends GuardContract<infer A>
? A
loginAs(
user: {
[K in keyof Authenticators]: Authenticators[K] extends GuardFactory
? ReturnType<Authenticators[K]> extends GuardContract<infer A>
? A
: never
: never
: never
}): Promise<void>
}[keyof Authenticators],
...args: any[]
): Promise<void>

/**
* Define the authentication guard for login
Expand All @@ -42,9 +45,13 @@ declare module 'playwright' {
* Login a user using a specific auth guard
*/
loginAs(
user: Authenticators[K] extends GuardFactory
? ReturnType<Authenticators[K]> extends GuardContract<infer A>
? A
user: ReturnType<Authenticators[K]> extends GuardContract<infer A> ? A : never,
...args: ReturnType<Authenticators[K]> extends GuardContract<infer A>
? ReturnType<Authenticators[K]>['authenticateAsClient'] extends (
_: A,
...args: infer Args
) => any
? Args
: never
: never
): Promise<void>
Expand All @@ -70,10 +77,10 @@ export const authBrowserClient = (app: ApplicationService) => {
*/
context.withGuard = function (guardName) {
return {
async loginAs(user) {
async loginAs(...args) {
const client = auth.createAuthenticatorClient()
const guard = client.use(guardName) as GuardContract<unknown>
const requestData = await guard.authenticateAsClient(user)
const requestData = await guard.authenticateAsClient(...args)

if (requestData.headers) {
throw new RuntimeException(
Expand All @@ -100,10 +107,10 @@ export const authBrowserClient = (app: ApplicationService) => {
* Login a user using the default authentication guard when
* using the browser context to make page visits
*/
context.loginAs = async function (user) {
context.loginAs = async function (user, ...args) {
const client = auth.createAuthenticatorClient()
const guard = client.use() as GuardContract<unknown>
const requestData = await guard.authenticateAsClient(user)
const requestData = await guard.authenticateAsClient(user, ...args)

if (requestData.headers) {
throw new RuntimeException(`Cannot use "${guard.driverName}" guard with browser client`)
Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ export interface GuardContract<User> {
* The method is used to authenticate the user as client.
* This method should return cookies, headers, or
* session state.
*
* The rest of the arguments can be anything the guard wants
* to accept
*/
authenticateAsClient(user: User): Promise<AuthClientResponse>
authenticateAsClient(user: User, ...args: any[]): Promise<AuthClientResponse>

/**
* Aymbol for infer the events emitted by a specific
Expand Down
4 changes: 2 additions & 2 deletions tests/access_tokens/guard/authenticate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ test.group('Access tokens guard | authenticate', () => {
const emitter = createEmitter()
const userProvider = new AccessTokensFakeUserProvider()
const user = await userProvider.findById(1)
const token = await userProvider.createToken(user!.getOriginal(), '20 mins')
const token = await userProvider.createToken(user!.getOriginal(), ['*'], '20 mins')
timeTravel(21 * 60)

ctx.request.request.headers.authorization = `Bearer ${token.value!.release()}`
Expand Down Expand Up @@ -278,7 +278,7 @@ test.group('Access tokens guard | authenticate', () => {

const guard = new AccessTokensGuard('api', ctx, emitter, userProvider)
const user = await userProvider.findById(1)
const token = await userProvider.createToken(user!.getOriginal(), '20 mins')
const token = await userProvider.createToken(user!.getOriginal(), ['*'], '20 mins')
await assert.rejects(() => guard.authenticate(), 'Unauthorized access')

ctx.request.request.headers.authorization = `Bearer ${token.value!.release()}`
Expand Down
Loading

0 comments on commit d8a3a77

Please sign in to comment.