mirror of
https://github.com/toptal/haste-server.git
synced 2024-11-28 12:55:57 +01:00
fix code, readme and lint
This commit is contained in:
parent
42c60c64c2
commit
350abbdf3b
33
.eslintrc.js
33
.eslintrc.js
@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
@ -10,47 +10,44 @@ module.exports = {
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings',
|
||||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
],
|
||||
plugins: [
|
||||
'import',
|
||||
'@typescript-eslint'
|
||||
'prettier'
|
||||
],
|
||||
plugins: ['import', '@typescript-eslint'],
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts'],
|
||||
'@typescript-eslint/parser': ['.ts']
|
||||
},
|
||||
'import/resolver': {
|
||||
node: {
|
||||
extensions: ['.js', '.ts'],
|
||||
moduleDirectory: ['node_modules', 'src/'],
|
||||
moduleDirectory: ['node_modules', 'src/']
|
||||
},
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
project: '.',
|
||||
},
|
||||
},
|
||||
project: '.'
|
||||
}
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
env: {
|
||||
jest: true,
|
||||
jest: true
|
||||
},
|
||||
files: ['**/__tests__/**/*.[jt]s', '**/?(*.)+(spec|test).[jt]s'],
|
||||
extends: ['plugin:jest/recommended'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'off',
|
||||
{ devDependencies: ['**/?(*.)+(spec|test).[jt]s'] },
|
||||
{ devDependencies: ['**/?(*.)+(spec|test).[jt]s'] }
|
||||
],
|
||||
camelcase: ['off'],
|
||||
},
|
||||
},
|
||||
camelcase: ['off']
|
||||
}
|
||||
}
|
||||
],
|
||||
ignorePatterns: ['**/*.js', 'node_modules', 'dist'],
|
||||
parserOptions: {
|
||||
root: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
project: ['./tsconfig.json']
|
||||
}
|
||||
}
|
||||
|
14
.github/workflows/close-inactive.yaml
vendored
14
.github/workflows/close-inactive.yaml
vendored
@ -2,7 +2,7 @@ name: Close inactive issues and PRs
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
close-stale:
|
||||
@ -15,16 +15,16 @@ jobs:
|
||||
with:
|
||||
days-before-stale: 30
|
||||
days-before-close: 14
|
||||
stale-issue-label: "stale"
|
||||
stale-pr-label: "stale"
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
|
||||
exempt-issue-labels: backlog,triage,nostale
|
||||
exempt-pr-labels: backlog,triage,nostale
|
||||
|
||||
stale-pr-message: "This PR is stale because it has been open for 30 days with no activity."
|
||||
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale."
|
||||
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity.'
|
||||
close-pr-message: 'This PR was closed because it has been inactive for 14 days since being marked as stale.'
|
||||
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity.'
|
||||
close-issue-message: 'This issue was closed because it has been inactive for 14 days since being marked as stale.'
|
||||
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
static
|
||||
/node_modules
|
59
README.md
59
README.md
@ -7,9 +7,9 @@ version can be found at [hastebin.com](http://hastebin.com)
|
||||
|
||||
Major design objectives:
|
||||
|
||||
* Be really pretty
|
||||
* Be really simple
|
||||
* Be easy to set up and use
|
||||
- Be really pretty
|
||||
- Be really simple
|
||||
- Be easy to set up and use
|
||||
|
||||
Haste works really well with a little utility called
|
||||
[haste-client](https://github.com/seejohnrun/haste-client), allowing you
|
||||
@ -22,54 +22,54 @@ STDOUT. Check the README there for more details and usages.
|
||||
|
||||
## Tested Browsers
|
||||
|
||||
* Firefox 8
|
||||
* Chrome 17
|
||||
* Safari 5.3
|
||||
- Firefox 8
|
||||
- Chrome 17
|
||||
- Safari 5.3
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the package, and expand it
|
||||
3. `yarn add`
|
||||
2. `yarn`
|
||||
|
||||
## Running the project
|
||||
|
||||
> Explore the settings inside of config.js, but the defaults should be good
|
||||
> Explore the settings inside of project-config.js, but the defaults should be good
|
||||
|
||||
### Development
|
||||
|
||||
1. `yarn add`
|
||||
1. `yarn`
|
||||
2. `yarn dev` (you may specify an optional `<config-path>` as well)
|
||||
|
||||
### Production
|
||||
|
||||
1. `yarn add`
|
||||
1. `yarn`
|
||||
2. `yarn build` to build the package
|
||||
3. `yarn start` to start the server
|
||||
|
||||
### Production with Docker
|
||||
|
||||
1. `docker compose up`
|
||||
1. `docker-compose up`
|
||||
|
||||
## Settings
|
||||
|
||||
* `host` - the host the server runs on (default localhost)
|
||||
* `port` - the port the server runs on (default 7777)
|
||||
* `keyLength` - the length of the keys to user (default 10)
|
||||
* `maxLength` - maximum length of a paste (default 400000)
|
||||
* `staticMaxAge` - max age for static assets (86400)
|
||||
* `recompressStaticAssets` - whether or not to compile static js assets (true)
|
||||
* `documents` - static documents to serve (ex: http://hastebin.com/about.com)
|
||||
- `host` - the host the server runs on (default localhost)
|
||||
- `port` - the port the server runs on (default 7777)
|
||||
- `keyLength` - the length of the keys to user (default 10)
|
||||
- `maxLength` - maximum length of a paste (default 400000)
|
||||
- `staticMaxAge` - max age for static assets (86400)
|
||||
- `recompressStaticAssets` - whether or not to compile static js assets (true)
|
||||
- `documents` - static documents to serve (ex: http://hastebin.com/about.com)
|
||||
in addition to static assets. These will never expire.
|
||||
* `storage` - storage options (see below)
|
||||
* `logging` - logging preferences
|
||||
* `keyGenerator` - key generator options (see below)
|
||||
* `rateLimits` - settings for rate limiting (see below)
|
||||
- `storage` - storage options (see below)
|
||||
- `logging` - logging preferences
|
||||
- `keyGenerator` - key generator options (see below)
|
||||
- `rateLimits` - settings for rate limiting (see below)
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
When present, the `rateLimits` option enables built-in rate limiting courtesy
|
||||
of `connect-ratelimit`. Any of the options supported by that library can be
|
||||
used and set in `config.js`.
|
||||
used and set in `project-config.js`.
|
||||
|
||||
See the README for [connect-ratelimit](https://github.com/dharmafly/connect-ratelimit)
|
||||
for more information!
|
||||
@ -104,7 +104,7 @@ for the key.
|
||||
|
||||
### File
|
||||
|
||||
To use file storage (the default) change the storage section in `config.js` to
|
||||
To use file storage (the default) change the storage section in `project-config.js` to
|
||||
something like:
|
||||
|
||||
```json
|
||||
@ -280,10 +280,7 @@ your bucket:
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject"
|
||||
],
|
||||
"Action": ["s3:GetObject", "s3:PutObject"],
|
||||
"Effect": "Allow",
|
||||
"Resource": "arn:aws:s3:::your-bucket-name-goes-here/*"
|
||||
}
|
||||
@ -401,6 +398,6 @@ SOFTWARE
|
||||
|
||||
### Other components:
|
||||
|
||||
* jQuery: MIT/GPL license
|
||||
* highlight.js: Copyright © 2006, Ivan Sagalaev
|
||||
* highlightjs-coffeescript: WTFPL - Copyright © 2011, Dmytrii Nagirniak
|
||||
- jQuery: MIT/GPL license
|
||||
- highlight.js: Copyright © 2006, Ivan Sagalaev
|
||||
- highlightjs-coffeescript: WTFPL - Copyright © 2011, Dmytrii Nagirniak
|
||||
|
15
about.md
15
about.md
@ -24,17 +24,18 @@ and send it to people.
|
||||
You can even take this a step further, and cut out the last step of copying the
|
||||
URL with:
|
||||
|
||||
* osx: `cat something | haste | pbcopy`
|
||||
* linux: `cat something | haste | xsel`
|
||||
* windows: check out [WinHaste](https://github.com/ajryan/WinHaste)
|
||||
- osx: `cat something | haste | pbcopy`
|
||||
- linux: `cat something | haste | xsel`
|
||||
- windows: check out [WinHaste](https://github.com/ajryan/WinHaste)
|
||||
|
||||
After running that, the STDOUT output of `cat something` will show up at a URL
|
||||
which has been conveniently copied to your clipboard.
|
||||
|
||||
That's all there is to that, and you can install it with `gem install haste`
|
||||
right now.
|
||||
* osx: you will need to have an up to date version of Xcode
|
||||
* linux: you will need to have rubygems and ruby-devel installed
|
||||
|
||||
- osx: you will need to have an up to date version of Xcode
|
||||
- linux: you will need to have rubygems and ruby-devel installed
|
||||
|
||||
## Duration
|
||||
|
||||
@ -52,8 +53,8 @@ pastes.
|
||||
|
||||
Haste can easily be installed behind your network, and it's all open source!
|
||||
|
||||
* [haste-client](https://github.com/seejohnrun/haste-client)
|
||||
* [haste-server](https://github.com/seejohnrun/haste-server)
|
||||
- [haste-client](https://github.com/seejohnrun/haste-client)
|
||||
- [haste-server](https://github.com/seejohnrun/haste-server)
|
||||
|
||||
## Author
|
||||
|
||||
|
@ -5,10 +5,8 @@ module.exports = {
|
||||
rootDir: '../',
|
||||
testRegex: '\\.test\\.ts$',
|
||||
reporters: ['default'],
|
||||
roots: [
|
||||
"test"
|
||||
],
|
||||
roots: ['test'],
|
||||
moduleNameMapper: {
|
||||
"src/(.*)": "<rootDir>/src/$1"
|
||||
'src/(.*)': '<rootDir>/src/$1'
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ const {
|
||||
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS,
|
||||
RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS,
|
||||
RATE_LIMITS_BLACKLIST,
|
||||
DOCUMENTS,
|
||||
} = process.env;
|
||||
DOCUMENTS
|
||||
} = process.env
|
||||
|
||||
const config = {
|
||||
host: HOST,
|
||||
@ -47,29 +47,29 @@ const config = {
|
||||
{
|
||||
level: LOGGING_LEVEL,
|
||||
type: LOGGING_TYPE,
|
||||
colorize: LOGGING_COLORIZE,
|
||||
},
|
||||
colorize: LOGGING_COLORIZE
|
||||
}
|
||||
],
|
||||
|
||||
keyGenerator: {
|
||||
type: KEYGENERATOR_TYPE,
|
||||
keyspace: KEY_GENERATOR_KEYSPACE,
|
||||
keyspace: KEY_GENERATOR_KEYSPACE
|
||||
},
|
||||
|
||||
rateLimits: {
|
||||
whitelist: RATE_LIMITS_WHITELIST ? RATE_LIMITS_WHITELIST.split(",") : [],
|
||||
blacklist: RATE_LIMITS_BLACKLIST ? RATE_LIMITS_BLACKLIST.split(",") : [],
|
||||
whitelist: RATE_LIMITS_WHITELIST ? RATE_LIMITS_WHITELIST.split(',') : [],
|
||||
blacklist: RATE_LIMITS_BLACKLIST ? RATE_LIMITS_BLACKLIST.split(',') : [],
|
||||
categories: {
|
||||
normal: {
|
||||
totalRequests: RATE_LIMITS_NORMAL_TOTAL_REQUESTS,
|
||||
every: RATE_LIMITS_NORMAL_EVERY_MILLISECONDS,
|
||||
every: RATE_LIMITS_NORMAL_EVERY_MILLISECONDS
|
||||
},
|
||||
whitelist:
|
||||
RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS ||
|
||||
RATE_LIMITS_WHITELIST_TOTAL_REQUESTS
|
||||
? {
|
||||
totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS,
|
||||
every: RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS,
|
||||
every: RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS
|
||||
}
|
||||
: null,
|
||||
blacklist:
|
||||
@ -77,10 +77,10 @@ const config = {
|
||||
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS
|
||||
? {
|
||||
totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS,
|
||||
every: RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS,
|
||||
every: RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS
|
||||
}
|
||||
: null
|
||||
}
|
||||
: null,
|
||||
},
|
||||
},
|
||||
|
||||
storage: {
|
||||
@ -94,15 +94,15 @@ const config = {
|
||||
db: STORAGE_DB,
|
||||
user: STORAGE_USERNAME,
|
||||
password: STORAGE_PASSWORD,
|
||||
path: STORAGE_FILEPATH,
|
||||
path: STORAGE_FILEPATH
|
||||
},
|
||||
|
||||
documents: DOCUMENTS
|
||||
? DOCUMENTS.split(",").reduce((acc, item) => {
|
||||
const keyAndValueArray = item.replace(/\s/g, "").split("=");
|
||||
return { ...acc, [keyAndValueArray[0]]: keyAndValueArray[1] };
|
||||
? DOCUMENTS.split(',').reduce((acc, item) => {
|
||||
const keyAndValueArray = item.replace(/\s/g, '').split('=')
|
||||
return { ...acc, [keyAndValueArray[0]]: keyAndValueArray[1] }
|
||||
}, {})
|
||||
: null,
|
||||
};
|
||||
: null
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(config));
|
||||
console.log(JSON.stringify(config))
|
||||
|
@ -95,6 +95,6 @@
|
||||
"dev": "nodemon",
|
||||
"lint": "eslint src --fix",
|
||||
"types:check": "tsc --noEmit --pretty",
|
||||
"pretty": "prettier --write src"
|
||||
"pretty": "prettier --write ."
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class DocumentHandler {
|
||||
|
||||
maxLength?: number
|
||||
|
||||
public store: Store
|
||||
store: Store
|
||||
|
||||
keyGenerator: KeyGenerator
|
||||
|
||||
@ -55,7 +55,7 @@ class DocumentHandler {
|
||||
)
|
||||
}
|
||||
|
||||
public handlePost(request: Request, response: Response) {
|
||||
handlePost(request: Request, response: Response) {
|
||||
// const this = this
|
||||
let buffer = ''
|
||||
let cancelled = false
|
||||
@ -121,7 +121,7 @@ class DocumentHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public handleRawGet(request: Request, response: Response) {
|
||||
handleRawGet(request: Request, response: Response) {
|
||||
const key = request.params.id.split('.')[0]
|
||||
const skipExpire = !!this.config.documents[key]
|
||||
|
||||
|
@ -23,29 +23,29 @@ export type BaseStoreConfig = {
|
||||
|
||||
export interface MongoStoreConfig extends BaseStoreConfig {
|
||||
connectionUrl: string
|
||||
type: StoreNames.mongo
|
||||
type: StoreNames.Mongo
|
||||
}
|
||||
|
||||
export interface MemcachedStoreConfig extends BaseStoreConfig {
|
||||
host: string
|
||||
port: number
|
||||
type: StoreNames.memcached
|
||||
type: StoreNames.Memcached
|
||||
}
|
||||
|
||||
export interface FileStoreConfig extends BaseStoreConfig {
|
||||
path: string
|
||||
type: StoreNames.file
|
||||
type: StoreNames.File
|
||||
}
|
||||
|
||||
export interface AmazonStoreConfig extends BaseStoreConfig {
|
||||
bucket: string
|
||||
region: string
|
||||
type: StoreNames.amazons3
|
||||
type: StoreNames.AmazonS3
|
||||
}
|
||||
|
||||
export interface PostgresStoreConfig extends BaseStoreConfig {
|
||||
connectionUrl: string
|
||||
type: StoreNames.postgres
|
||||
type: StoreNames.Postgres
|
||||
}
|
||||
|
||||
export interface RethinkDbStoreConfig extends BaseStoreConfig {
|
||||
@ -54,7 +54,7 @@ export interface RethinkDbStoreConfig extends BaseStoreConfig {
|
||||
db: string
|
||||
user: string
|
||||
password: string
|
||||
type: StoreNames.rethinkdb
|
||||
type: StoreNames.RethinkDb
|
||||
}
|
||||
|
||||
export interface RedisStoreConfig extends BaseStoreConfig {
|
||||
@ -65,11 +65,11 @@ export interface RedisStoreConfig extends BaseStoreConfig {
|
||||
password?: string
|
||||
host?: string
|
||||
port?: string
|
||||
type: StoreNames.redis
|
||||
type: StoreNames.Redis
|
||||
}
|
||||
|
||||
export interface GoogleStoreConfig extends BaseStoreConfig {
|
||||
type: StoreNames.googledatastore
|
||||
type: StoreNames.GoogleDataStore
|
||||
}
|
||||
|
||||
export type StoreConfig =
|
||||
|
@ -1,11 +1,11 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export enum StoreNames {
|
||||
amazons3 = 'amazon-s3',
|
||||
file = 'file',
|
||||
googledatastore = 'google-datastore',
|
||||
memcached = 'memcached',
|
||||
mongo = 'mongo',
|
||||
postgres = 'postgres',
|
||||
redis = 'redis',
|
||||
rethinkdb = 'rethinkdb'
|
||||
AmazonS3 = 'amazon-s3',
|
||||
File = 'file',
|
||||
GoogleDataStore = 'google-datastore',
|
||||
Memcached = 'memcached',
|
||||
Mongo = 'mongo',
|
||||
Postgres = 'postgres',
|
||||
Redis = 'redis',
|
||||
RethinkDb = 'rethinkdb'
|
||||
}
|
||||
|
@ -1,25 +1,35 @@
|
||||
import { createMock } from 'ts-auto-mock';
|
||||
import { createMock } from 'ts-auto-mock'
|
||||
import DocumentHandler from 'src/lib/document-handler/index'
|
||||
import Generator from 'src/lib/key-generators/random'
|
||||
import constants from 'src/constants'
|
||||
import { Config } from 'src/types/config'
|
||||
import { Store } from 'src/lib/document-stores';
|
||||
import { Store } from 'src/lib/document-stores'
|
||||
|
||||
const store : Store = createMock<Store>();
|
||||
const config : Config = createMock<Config>();
|
||||
const store: Store = createMock<Store>()
|
||||
const config: Config = createMock<Config>()
|
||||
|
||||
describe('document-handler', () => {
|
||||
describe('with random key', () => {
|
||||
it('should choose a key of the proper length', () => {
|
||||
const gen = new Generator({ type: 'random' })
|
||||
const dh = new DocumentHandler({ keyLength: 6, keyGenerator: gen, store, config})
|
||||
expect(dh.acceptableKey()?.length).toEqual(6);
|
||||
const dh = new DocumentHandler({
|
||||
keyLength: 6,
|
||||
keyGenerator: gen,
|
||||
store,
|
||||
config
|
||||
})
|
||||
expect(dh.acceptableKey()?.length).toEqual(6)
|
||||
})
|
||||
|
||||
it('should choose a default key length', () => {
|
||||
const gen = new Generator({ type: 'random' })
|
||||
const dh = new DocumentHandler({ keyGenerator: gen, maxLength: 1, store, config })
|
||||
expect(dh.keyLength).toEqual(constants.DEFAULT_KEY_LENGTH);
|
||||
const dh = new DocumentHandler({
|
||||
keyGenerator: gen,
|
||||
maxLength: 1,
|
||||
store,
|
||||
config
|
||||
})
|
||||
expect(dh.keyLength).toEqual(constants.DEFAULT_KEY_LENGTH)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -14,7 +14,7 @@ describe('Redis document store', () => {
|
||||
it('should be able to set a key and have an expiration set', async () => {
|
||||
store = new RedisDocumentStore({
|
||||
expire: 10,
|
||||
type: StoreNames.redis
|
||||
type: StoreNames.Redis
|
||||
})
|
||||
return store.set('hello1', 'world', async () => {
|
||||
const res = await store.client?.ttl('hello1')
|
||||
@ -25,7 +25,7 @@ describe('Redis document store', () => {
|
||||
it('should not set an expiration when told not to', async () => {
|
||||
store = new RedisDocumentStore({
|
||||
expire: 10,
|
||||
type: StoreNames.redis
|
||||
type: StoreNames.Redis
|
||||
})
|
||||
|
||||
store.set(
|
||||
@ -41,7 +41,7 @@ describe('Redis document store', () => {
|
||||
|
||||
it('should not set an expiration when expiration is off', async () => {
|
||||
store = new RedisDocumentStore({
|
||||
type: StoreNames.redis
|
||||
type: StoreNames.Redis
|
||||
})
|
||||
|
||||
store.set('hello3', 'world', async () => {
|
||||
|
@ -1,19 +1,19 @@
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
import Generator from 'src/lib/key-generators/phonetic'
|
||||
|
||||
const vowels = 'aeiou';
|
||||
const consonants = 'bcdfghjklmnpqrstvwxyz';
|
||||
const vowels = 'aeiou'
|
||||
const consonants = 'bcdfghjklmnpqrstvwxyz'
|
||||
|
||||
describe('PhoneticKeyGenerator', () => {
|
||||
describe('generation', () => {
|
||||
it('should return a key of the proper length', () => {
|
||||
const gen = new Generator({ type: 'phonetic'});
|
||||
expect(gen.createKey(6).length).toEqual(6);
|
||||
});
|
||||
const gen = new Generator({ type: 'phonetic' })
|
||||
expect(gen.createKey(6).length).toEqual(6)
|
||||
})
|
||||
|
||||
it('should alternate consonants and vowels', () => {
|
||||
const gen = new Generator({ type: 'phonetic'});
|
||||
const key = gen.createKey(3);
|
||||
const gen = new Generator({ type: 'phonetic' })
|
||||
const key = gen.createKey(3)
|
||||
// if it starts with a consonant, we expect cvc
|
||||
// if it starts with a vowel, we expect vcv
|
||||
if (consonants.includes(key[0])) {
|
||||
@ -25,6 +25,6 @@ describe('PhoneticKeyGenerator', () => {
|
||||
expect(vowels.includes(key[2])).toBeTruthy()
|
||||
expect(consonants.includes(key[1])).toBeTruthy()
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user