Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
maticzav committed Jun 10, 2023
1 parent ce4328f commit 690fcaa
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 30 deletions.
61 changes: 37 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ Each sessions variant implements a general `ISessions` interface and may be swap

> NOTE: Examples below use `trpc` and `fastify`, but the API is completely framework agnostic and should be easy to port to any other server framework.
````ts
#### Defining Types

```ts
type SessionId = number
type SessionMeta = {
// anything that's serializable
// anything that's serializable
}
```
#### Creating Session Stores
```ts
// InMemorySessions (best for local development).
const sessions = new InMemorySessions<string, SessionMeta>()

Expand All @@ -31,46 +37,50 @@ const redis: RedisClientType = createClient({
})

const sessions = new RedisSessions<string, SessionMeta>({ redis })
```

#### Creating Context

By creating a context, you let the routes in your GraphQL or REST server access the same store.

> I usually incldue the parsed session information in the context as well, so that I don't need to repeat it in each route separately.
```ts
export type Context = {
/**
* The session ID of the authenticated user if there exists one.
*/
session: SessionId | null

/**
* Access to session related services.
*/
sessions: ISessions<string, SessionMeta>
}


// Create a context with a shared sessions instance.
await server.register(fastifyTRPCPlugin, {
prefix: '/trpc',
logLevel: 'debug',
trpcOptions: {
router: root,
createContext: ({ req, res }: CreateFastifyContextOptions): Context => {
const session = SessionUtils.getSessionIdFromAuthToken(req.headers.authorization)

return { sessions, session }
},
},
prefix: '/trpc',
logLevel: 'debug',
trpcOptions: {
router: root,
createContext: ({ req, res }: CreateFastifyContextOptions): Context => {
const session = SessionUtils.getSessionIdFromAuthToken(req.headers.authorization)

return { sessions, session }
},
},
})
```

#### Manipulating Sessions

```ts
// Getting session from request header.
const session = SessionUtils.getSessionIdFromAuthToken(req.headers.authorization)

// Getting Session Information
const userId = await ctx.sessions.getUserIdFromSession(session)
const meta = await ctx.sessions.getSessionMeta(session)

// Creating sessions and getting tokens.
const session = await ctx.sessions.createSession({
userId: user.id,
label: input.label,
meta: {},
userId: user.id,
label: input.label,
meta: {},
})
const token = SessionUtils.getAuthTokenForSessionId(session)

Expand All @@ -88,4 +98,7 @@ const allExistingSessions = await sessions.listSessions()
### License

MIT @ Matic Zavadlal
````

```
```
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: '3.1'
services:

redis:
build:
context: .
dockerfile: ./docker/redis/Dockerfile
restart: always
ports:
- "6379:6379"
environment:
REDIS_PASSWORD: redis
volumes:
- redis:/data

volumes:
postgres:
redis:
6 changes: 6 additions & 0 deletions docker/redis/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM redis:7-alpine

COPY docker/redis/redis.conf /usr/local/etc/redis/redis.conf
COPY docker/redis/start.sh /usr/bin/start.sh

CMD ["/usr/bin/start.sh"]
18 changes: 18 additions & 0 deletions docker/redis/redis.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Important Redis defaults (for reference):
# -----------------------------------------
# bind 127.0.0.1 -::1
# port 6379
# save 3600 1
# save 300 100
# save 60 10000
# appendonly no
# dir /var/lib/redis
# maxmemory <no default!>
# maxmemory-policy noeviction

bind 0.0.0.0 -::1

dir /data
maxmemory-policy noeviction
maxmemory 460mb
appendonly no
17 changes: 17 additions & 0 deletions docker/redis/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh

# This script is inspired by https://github.com/mra-clubhouse/fly-redis/tree/custom-redis.

# Clear environment variables.
set -e

sysctl vm.overcommit_memory=1 || true
sysctl net.core.somaxconn=1024 || true

if [[ -z "${REDIS_PASSWORD}" ]]; then
echo "The REDIS_PASSWORD environment variable is required"
exit 1
fi


redis-server /usr/local/etc/redis/redis.conf --requirepass $REDIS_PASSWORD
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"test:lib": "jest"
},
"dependencies": {
"ioredis": "^5.3.1",
"luxon": "^3.2.1",
"redis": "^4.6.5",
"uuid": "^9.0.0"
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { InMemorySessions } from './inmemory'
export { RedisSessions } from './redis'
export { InMemorySessions } from './stores/inmemory'
export { RedisSessions } from './stores/redis'

export { SessionUtils } from './utils'

export type { ISessions, Session, SessionId } from './types'
2 changes: 1 addition & 1 deletion src/inmemory.ts → src/stores/inmemory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as crypto from 'crypto'
import { DateTime } from 'luxon'

import { Session, SessionId, ISessions } from './types'
import { Session, SessionId, ISessions } from '../types'

/**
* Utility class that manages user sessions in memory.
Expand Down
2 changes: 1 addition & 1 deletion src/redis.ts → src/stores/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as crypto from 'crypto'
import { DateTime } from 'luxon'
import { RedisClientType } from 'redis'

import { Session, SessionId, ISessions } from './types'
import { Session, SessionId, ISessions } from '../types'

/**
* Key containing all active session identifiers.
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export namespace SessionUtils {
* Returns the HTTP Authorization header that should be used to associate a given session.
*/
export function getAuthTokenForSessionId(sessionId: SessionId): string {
return sessionId as string
return `Bearer ${sessionId}`
}

/**
Expand Down
Empty file removed tests/inmemory.test.ts
Empty file.
Empty file removed tests/redis.test.ts
Empty file.
143 changes: 143 additions & 0 deletions tests/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { InMemorySessions } from '../src/stores/inmemory'
import { SessionUtils } from '../src/utils'
import { filter } from './utils'

describe('inmemory', () => {
test('correctly finds user from a given session id', async () => {
const sessions = new InMemorySessions<string, {}>()

const validSessionId = await sessions.createSession({
userId: 'user-id',
label: 'test',
meta: {},
})

await expect(sessions.getUserIdFromSession(validSessionId)).resolves.not.toBeNull()

const invalidSessionId = SessionUtils.toSessionId('invalid-session-id')

await expect(sessions.getUserIdFromSession(invalidSessionId)).resolves.toBeNull()
})

test('correctly parses userId from a given session id', async () => {
const sessions = new InMemorySessions<string, {}>()

const userId = 'user-id'
const sessionId = await sessions.createSession({
userId,
label: 'test',
meta: {},
})

await expect(sessions.getUserIdFromSession(sessionId)).resolves.toEqual(userId)
})

test('correctly parses meta from a given session id', async () => {
// NOTE: We want to test all serializable types here.

const sessions = new InMemorySessions<
string,
{
string: string
number: number
boolean: boolean
array: string[]
object: { [key: string]: string }
}
>()

const meta = {
string: 'string',
number: 1,
boolean: true,
array: ['string', 'string'],
object: {
string: 'string',
},
}

const sessionId = await sessions.createSession({
userId: 'user-id',
label: 'test',
meta,
})

await expect(sessions.getSessionMeta(sessionId)).resolves.toEqual(meta)
})

test('correctly destroys a session', async () => {
const sessions = new InMemorySessions<string, {}>()

const sessionId = await sessions.createSession({
userId: 'user-id',
label: 'test',
meta: {},
})

await sessions.destroySession(sessionId)

await expect(sessions.getUserIdFromSession(sessionId)).resolves.toBeNull()
})

test('correctly lists all sessions', async () => {
const sessions = new InMemorySessions<string, {}>()

await sessions.createSession({ userId: 'user-id', label: '#1', meta: {} })
await sessions.createSession({ userId: 'user-id', label: '#2', meta: {} })
await sessions.createSession({ userId: 'user-id', label: '#3', meta: {} })

await expect(
// We filter out random values and dates.
sessions.listSessions().then((r) => r.map((o) => filter(o, ['label', 'userId']))),
).resolves.toMatchInlineSnapshot(`
[
{
"label": "#1",
"userId": "user-id",
},
{
"label": "#2",
"userId": "user-id",
},
{
"label": "#3",
"userId": "user-id",
},
]
`)
})

test('correclty lists sessions for a given user', async () => {
const sessions = new InMemorySessions<string, {}>()

const userId = 'user-id'

await sessions.createSession({ userId, label: '#1', meta: {} })
await sessions.createSession({ userId, label: '#2', meta: {} })
await sessions.createSession({ userId, label: '#3', meta: {} })

const otherUserId = 'other-user-id'
await sessions.createSession({ userId: otherUserId, label: '#1', meta: {} })
await sessions.createSession({ userId: otherUserId, label: '#2', meta: {} })

await expect(
// We filter out random values and dates.
sessions.getSessionsForUser(userId).then((r) => r.map((o) => filter(o, ['label', 'userId']))),
).resolves.toMatchInlineSnapshot(`
[
{
"label": "#1",
"userId": "user-id",
},
{
"label": "#2",
"userId": "user-id",
},
{
"label": "#3",
"userId": "user-id",
},
]
`)
})
})
13 changes: 13 additions & 0 deletions tests/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SessionUtils } from '../src/utils'

describe('utils', () => {
test('correctly parses and formats session auth token', () => {
const random = Math.random().toString(16)
const sessionId = SessionUtils.toSessionId(random)

const authToken = SessionUtils.getAuthTokenForSessionId(sessionId)
const parsedSessionId = SessionUtils.getSessionIdFromAuthToken(authToken)

expect(parsedSessionId).toEqual(sessionId)
})
})
10 changes: 10 additions & 0 deletions tests/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Filters out desired keys from an object.
*/
export function filter<T extends object, KS extends keyof T>(obj: T, keys: KS[]): Pick<T, KS> {
const result: any = {}
for (const key of keys) {
result[key] = obj[key]
}
return result
}

0 comments on commit 690fcaa

Please sign in to comment.