Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: bahmutov/start-server-and-test
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 7a9f94b8b2e95e8b81741104aa79063055394a11
Choose a base ref
...
head repository: bahmutov/start-server-and-test
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: ad35c2e4128615d7bc6e84d09761ae5d1fbc8da2
Choose a head ref

Commits on Mar 19, 2021

  1. Copy the full SHA
    3f1ed47 View commit details

Commits on May 14, 2021

  1. Update README.md (#302)

    svenheden authored May 14, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    cfdf651 View commit details
  2. Update README.md with Apollo Server notes (#297)

    * add notes regarding Apollo Server usage
    
    * fix grammar
    
    * add more generic highlighted JSON example
    JGJP authored May 14, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0bb9702 View commit details
  3. fix(deps): update dependency execa to v5 (#290)

    Co-authored-by: Renovate Bot <bot@renovateapp.com>
    renovate[bot] and renovate-bot authored May 14, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    a131819 View commit details

Commits on May 24, 2021

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    d37318e View commit details
  2. Copy the full SHA
    445fe2f View commit details

Commits on Jun 4, 2021

  1. Copy the full SHA
    c546d00 View commit details

Commits on Jun 6, 2021

  1. Copy the full SHA
    b6b09bc View commit details

Commits on Jul 6, 2021

  1. Copy the full SHA
    3b17fb0 View commit details

Commits on Jul 22, 2021

  1. feat(api): adds check for http status 304 (#311)

    * feat(api): adds check for http status 304
    
    http status code should be considered successful
    
    * feat(api): adds tests for http status code 304
    cmcnicholas authored Jul 22, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c0adf51 View commit details

Commits on Jul 26, 2021

  1. fix(deps): update dependency wait-on to v6 (#307)

    Co-authored-by: Renovate Bot <bot@renovateapp.com>
    renovate[bot] and renovate-bot authored Jul 26, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    eeaaf92 View commit details

Commits on Aug 3, 2021

  1. explain the server not responding (#314)

    and the workarounds
    bahmutov authored Aug 3, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7ac144f View commit details

Commits on Sep 2, 2021

  1. FEAT: Allow interval option from environment variable (#315)

    * Allow interval option from environment variable
    
    * feat(test): add new demo for testing, update README
    cpow authored Sep 2, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    693dc2c View commit details

Commits on Dec 5, 2022

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    776c7b6 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7294173 View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    6bedc4c View commit details
  4. feat: use the --expect parameter to specify HTTP response code (#343)

    * feat: add --expect argument parsing
    
    * feat: specify the --expected http status code
    
    * chore: update the README
    bahmutov authored Dec 5, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    17a427c View commit details

Commits on Dec 7, 2022

  1. Copy the full SHA
    9cfb60e View commit details

Commits on Dec 9, 2022

  1. Copy the full SHA
    4e33599 View commit details

Commits on Jan 23, 2023

  1. fix(deps): update dependency wait-on to v7 (#347)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Jan 23, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2ebf1ba View commit details

Commits on Feb 13, 2023

  1. Copy the full SHA
    9e8713b View commit details

Commits on Feb 22, 2023

  1. split ci workflow jobs (#353)

    * split ci workflow jobs
    
    * split one test job into two
    bahmutov authored Feb 22, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c5d7066 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f418050 View commit details

Commits on Feb 24, 2023

  1. confirm ::1 address works (#355)

    * confirm ::1 address works
    
    * name of the script
    bahmutov authored Feb 24, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0e9984e View commit details

Commits on Feb 26, 2023

  1. fix: pinging the server at 127.0.0.1 when using just the port number (#…

    …357)
    
    * add 0.0.0.0 server ci example
    
    * add explicit zero test
    
    * add timeout to the waiting failing demo
    
    * add test and explanation
    
    * run more tests in Node v16
    
    * add debugging
    
    * update what we run on node v16
    
    * switch from localhost to 127.0.0.1 for default host
    
    * prepend localhost and common host names with http
    
    * update readme
    
    * ipv6 test
    
    * update localhost in README snippets
    bahmutov authored Feb 26, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    9df0423 View commit details

Commits on Feb 27, 2023

  1. feat: making the default host 127.0.0.1 (#359)

    * feat: upgrade to v2
    
    BREAKING CHANGE: default host to ping is 127.0.0.1
    
    * code format
    bahmutov authored Feb 27, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    a9e395c View commit details

Commits on Sep 21, 2023

  1. Fix: fix the interactive shell in the test process (#369)

    Fixes the issue that interactive stdin in the test process was erratic, due to that the main process's stdin was piped to both the server and the test processes.
    sarimarton authored Sep 21, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    04b33b3 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2d1decf View commit details
  3. Verified

    This commit was signed with the committer’s verified signature.
    bahmutov Gleb Bahmutov
    Copy the full SHA
    2fc1f98 View commit details

Commits on Nov 5, 2023

  1. Copy the full SHA
    efe7384 View commit details

Commits on Nov 16, 2023

  1. fix(deps): update dependency wait-on to v7.2.0 (#374)

    * fix(deps): update dependency wait-on to v7.2.0
    
    * fix: use public registry
    
    ---------
    
    Co-authored-by: Patrick Hardin <patrick-hardin@users.noreply.github.com>
    patrick-hardin and patrick-hardin authored Nov 16, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ad35c2e View commit details
Showing with 36,415 additions and 15,013 deletions.
  1. +127 −18 .github/workflows/ci.yml
  2. +21 −0 LICENSE
  3. +133 −33 README.md
  4. +55 −13 __snapshots__/utils-spec.js
  5. +35,780 −14,921 package-lock.json
  6. +23 −8 package.json
  7. +10 −3 src/bin/start.js
  8. +30 −7 src/index.js
  9. +52 −4 src/utils-spec.js
  10. +107 −6 src/utils.js
  11. +3 −0 test/data.json
  12. +17 −0 test/ip6.mjs
  13. +20 −0 test/server-304.js
  14. +20 −0 test/server-403.js
  15. +17 −0 test/zero.mjs
145 changes: 127 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,145 @@
name: ci
on: [push, pull_request]
jobs:
tests:
tests1:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v1
uses: actions/checkout@v4

- name: Node version 🖨️
run: node -v

- name: NPM install
uses: bahmutov/npm-install@v1

- name: Run tests 🧪
run: npm test

- name: Run demos 📊
run: |
npm run demo
npm run demo2
npm run demo3
npm run demo4
npm run demo5
npm run demo6
npm run demo7
# hmm why are some demos skipped?
npm run demo11
START_SERVER_AND_TEST_INSECURE=1 npm run demo9
npm run demo-cross-env
npm run demo-commands
npm run demo-multiple
- name: Run demo 1 📊
run: npm run demo

- name: Run demo 2 📊
run: npm run demo2

- name: Run demo 3 📊
run: npm run demo3

- name: Run demo 4 📊
run: npm run demo4

- name: Run demo 5 📊
run: npm run demo5

- name: Run demo 6 📊
run: npm run demo6

- name: Run demo 7 📊
run: npm run demo7

tests2:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v4

- name: Node version 🖨️
run: node -v

- name: NPM install
uses: bahmutov/npm-install@v1
# hmm why are some demos skipped?

- name: Run demo 11 📊
run: npm run demo11

- name: Run demo 12 📊
run: npm run demo12

- name: Run demo 9 📊
run: START_SERVER_AND_TEST_INSECURE=1 npm run demo9

- name: Run demo cross env 📊
run: npm run demo-cross-env

- name: Run demo commands 📊
run: npm run demo-commands

- name: Run demo multiple 📊
run: npm run demo-multiple

- name: Run demo expect 403 code 📊
run: npm run demo-expect-403

tests-node-v16:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v4

- uses: actions/setup-node@v3
with:
node-version: 16

- name: Node version 🖨️
run: node -v

- name: NPM install
uses: bahmutov/npm-install@v1

- name: Run json-server test
run: npm run demo-json-server

- name: Run explicit 0.0.0.0 host example
run: npm run demo-zero-explicit

- name: Run 0.0.0.0 host with 127.0.0.1 url
run: npm run demo-zero-127

- name: Run 0.0.0.0 host example
run: npm run demo-zero

tests-node-v18:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v4

- uses: actions/setup-node@v3
with:
node-version: 18

- name: Node version 🖨️
run: node -v

- name: NPM install
uses: bahmutov/npm-install@v1

- name: Run json-server test
run: npm run demo-json-server

- name: Run ::1 host example
run: npm run demo-ip6

- name: Run explicit 0.0.0.0 host example
run: npm run demo-zero-explicit

- name: Run 0.0.0.0 host with 127.0.0.1 url
run: npm run demo-zero-127

- name: Run 0.0.0.0 host example
run: npm run demo-zero

release:
needs: ['tests1', 'tests2', 'tests-node-v16', 'tests-node-v18']
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v4

- name: Semantic Release 🚀
uses: cycjimmy/semantic-release-action@v2
uses: cycjimmy/semantic-release-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 bahmutov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
166 changes: 133 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -11,12 +11,25 @@

## Install

Requires [Node](https://nodejs.org/en/) version 8.9 or above.
Requires [Node](https://nodejs.org/en/) version 16 or above.

```sh
npm install --save-dev start-server-and-test
```

## Upgrade

### v1 to v2

If you are using just the port number, and the resolved URL `localhost:xxxx` no longer works, use the explicit `http://localhost:xxxx` instead

```
# v1
$ npx start-test 3000
# v2
$ npx start-test http://localhost:3000
```

## Use

This command is meant to be used with NPM script commands. If you have a "start server", and "test" script names for example, you can start the server, wait for a url to respond, then run tests. When the test process exits, the server is shut down.
@@ -80,14 +93,17 @@ You can use either `start-server-and-test`, `server-test` or `start-test` comman
You can use `:` in front of port number like `server-test :8080`, so all these are equivalent

```
start-server-and-test start http://localhost:8080 test
server-test start http://localhost:8080 test
server-test http://localhost:8080 test
start-server-and-test start http://127.0.0.1:8080 test
server-test start http://127.0.0.1:8080 test
server-test http://127.0.0.1:8080 test
server-test 127.0.0.1:8080 test
start-test :8080 test
start-test 8080 test
start-test 8080
```

**Tip:** I highly recommend you specify the full url instead of the port, see the `localhost vs 0.0.0.0 vs 127.0.0.1` section later in this README.

### Options

If you use convention and name your scripts "start" and "test" you can simply provide URL
@@ -102,7 +118,7 @@ If you use convention and name your scripts "start" and "test" you can simply pr
}
```

You can also shorten local url to just port, the code below is equivalent to checking `http://localhost:8080`.
You can also shorten local url to just port, the code below is equivalent to checking `http://127.0.0.1:8080`.

```json
{
@@ -152,6 +168,33 @@ You can provide multiple resources to wait on, separated by a pipe `|`. _(be sur

or for multiple ports simply: `server-test '8000|9000' test`.

If you want to start the server, wait for it to respond, and then run multiple test commands (and stop the server after they finish), you should be able to use `&&` to separate the test commands:

```json
{
"scripts": {
"start": "npm start",
"test:unit": "mocha test.js",
"test:e2e": "mocha e2e.js",
"ci": "start-test 9000 'npm run test:unit && npm run test:e2e'"
}
}
```

The above script `ci` after the `127.0.0.1:9000` responds executes the `npm run test:unit` command. Then when it finishes it runs `npm run test:e2e`. If the first or second command fails, the `ci` script fails. Of course, your mileage on Windows might vary.

#### expected

The server might respond, but require authorization, returning an error HTTP code by default. You can still know that the server is responding by using `--expect` argument (or its alias `--expected`):

```
$ start-test --expect 403 start :9000 test:e2e
```

See `demo-expect-403` NPM script.

Default expected value is 200.

## `npx` and `yarn`

If you have [npx](https://www.npmjs.com/package/npx) available, you can execute locally installed tools from the shell. For example, if the `package.json` has the following local tools:
@@ -171,7 +214,7 @@ Then you can execute tests simply:
```text
$ npx start-test 'http-server -c-1 .' 8080 'cypress run'
starting server using command "http-server -c-1 ."
and when url "http://localhost:8080" is responding
and when url "http://127.0.0.1:8080" is responding
running tests using command "cypress run"
Starting up http-server, serving .
...
@@ -184,12 +227,36 @@ $ yarn start-test 'http-server -c-1 .' 8080 'cypress run'
yarn run v1.13.0
$ /private/tmp/test-t/node_modules/.bin/start-test 'http-server -c-1 .' 8080 'cypress run'
starting server using command "http-server -c-1 ."
and when url "http://localhost:8080" is responding
and when url "http://127.0.0.1:8080" is responding
running tests using command "cypress run"
Starting up http-server, serving .
...
```

## localhost vs 0.0.0.0 vs 127.0.0.1

The latest versions of Node and some web servers listen on host `0.0.0.0` which _no longer means localhost_. Thus if you specify _just the port number_, like `:3000`, this package will try `http://127.0.0.1:3000` to ping the server. A good practice is to specify the full URL you would like to ping.

```
# same as "http://127.0.0.1:3000"
start-server start 3000 test
# better
start-server start http://127.0.0.1:3000 test
# or
start-server start http://0.0.0.0:3000 test
# of course, if your server is listening on localhost
# you can still set the URL
start-server start http://localhost:3000 test
```

If you specify just `localhost` or `127.0.0.1` or `0.0.0.0`, it automatically pings `http://...` URL.

```
start-test localhost:3000
# is the same as
start-test http://localhost:3000
```

## Note for yarn users

By default, npm is used to run scripts, however you can specify that yarn is used as follows:
@@ -204,28 +271,70 @@ By default, npm is used to run scripts, however you can specify that yarn is use

## Note for webpack-dev-server users

If you are using [webpack-dev-server](https://www.npmjs.com/package/webpack-dev-server) (directly or via `angular/cli` or other boilerplates) then please use the following URL form to check
Also applies to **Vite** users!

If you are using [webpack-dev-server](https://www.npmjs.com/package/webpack-dev-server) (directly or via `angular/cli` or other boilerplates) then the server does not respond to HEAD requests from `start-server-and-test`. You can check if the server responds to the HEAD requests by starting the server and pinging it from another terminal using `curl`

```
# from the first terminal start the server
$ npm start
# from the second terminal call the server with HEAD request
$ curl --head http://localhost:3000
```

If the server responds with 404, then it does not handle the HEAD requests. You have two solutions:

### Use HTTP GET requests

You can force the `start-server-and-test` to ping the server using GET requests using the `http-get://` prefix:


```
start-server-and-test http-get://localhost:8080
```

This is because under the hood this module uses [wait-on](https://github.com/jeffbski/wait-on) to ping the server. Wait-on uses `HEAD` by default, but `webpack-dev-server` does not respond to `HEAD` only to `GET` requests. Thus you need to use `http-get://` URL format to force `wait-on` to use `GET` probe.
### Ping a specific resource

As an alternative to using GET method to request the root page, you can try pinging a specific resource, see the discussion in the [issue #4](https://github.com/bahmutov/start-server-and-test/issues/4).

```
# maybe the server responds to HEAD requests to the HTML page
start-server-and-test http://localhost:3000/index.html
# or maybe the server responds to HEAD requests to JS resource
start-server-and-test http://localhost:8080/app.js
```

### Explanation

You can watch the explanation in the video [Debug a Problem in start-server-and-test](https://youtu.be/rxyZOxYCsAk).

You can even wait on the bundle JavaScript url instead of the page url, see discussion in this [issue #4](https://github.com/bahmutov/start-server-and-test/issues/4)
Under the hood this module uses [wait-on](https://github.com/jeffbski/wait-on) to ping the server. Wait-on uses `HEAD` by default, but `webpack-dev-server` does not respond to `HEAD` only to `GET` requests. Thus you need to use `http-get://` URL format to force `wait-on` to use `GET` probe or ask for a particular resource.

### Debugging

To see diagnostic messages, run with environment variable `DEBUG=start-server-and-test`

```
$ DEBUG=start-server-and-test npm run test
start-server-and-test parsing CLI arguments: [ 'dev', '3000', 'subtask' ] +0ms
start-server-and-test parsed args: { services: [ { start: 'npm run dev', url: [Array] } ], test: 'npm run subtask' }
...
making HTTP(S) head request to url:http://127.0.0.1:3000 ...
HTTP(S) error for http://127.0.0.1:3000 Error: Request failed with status code 404
```

### Disable HTTPS certificate checks

To see disable HTTPS checks for `wait-on`, run with environment variable `START_SERVER_AND_TEST_INSECURE=1`.
To disable HTTPS checks for `wait-on`, run with environment variable `START_SERVER_AND_TEST_INSECURE=1`.

### Timeout

This utility will wait for maximum of 5 minutes while checking for the server to respond (default). Setting an environment variable `WAIT_ON_TIMEOUT=600000` (milliseconds) sets the timeout for example to 10 minutes.

### Interval

This utility will check for a server response every two seconds (default). Setting an environment variable `WAIT_ON_INTERVAL=600000` (milliseconds) sets the interval for example to 10 minutes.

### Starting two servers

Sometimes you need to start one API server and one webserver in order to test the application. Use the syntax:
@@ -249,6 +358,18 @@ For example if API runs at port 3000 and server runs at port 8080:

In the above example you would run `npm run test:all` to start the API first, then when it responds, start the server, and when the server is responding, it would run the tests. After the tests finish, it will shut down both servers. See the repo [start-two-servers-example](https://github.com/bahmutov/start-two-servers-example) for full example

## Note for Apollo Server users

When passing a simple GET request to Apollo Server it will respond with a 405 error. To get around this problem you need to pass a valid GraphQL query into the query parameter. Passing in a basic schema introspection query will work to determine the presence of an Apollo Server. You can configure your npm script like so:

```json
{
"scripts": {
"ci": "start-server-and-test start 'http-get://localhost:4000/graphql?query={ __schema { queryType { name } } }' test"
}
}
```

### Small print

Author: Gleb Bahmutov &lt;gleb.bahmutov@gmail.com&gt; &copy; 2017
@@ -264,28 +385,7 @@ Support: if you find any problems with this module, email / tweet /

## MIT License

Copyright (c) 2017 Gleb Bahmutov &lt;gleb.bahmutov@gmail.com&gt;

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
See [LICENSE](./LICENSE)

[npm-icon]: https://nodei.co/npm/start-server-and-test.svg?downloads=true
[npm-url]: https://npmjs.org/package/start-server-and-test
68 changes: 55 additions & 13 deletions __snapshots__/utils-spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
exports['utils crossArguments concates arguments if wrapped by " 1'] = [
"start",
"8080",
"test argument --option"
]

exports['utils crossArguments concates arguments if wrapped by \' 1'] = [
"start",
"8080",
"test argument --option"
]

exports['utils crossArguments concates arguments if wrapped by ` 1'] = [
"start",
"8080",
"test argument --option"
]

exports['utils crossArguments ignores end char (") if not at the end of an argument 1'] = [
"start",
"8080",
"test argu\"ment --option"
]

exports['utils crossArguments ignores end char (\') if not at the end of an argument 1'] = [
"start",
"8080",
"test argu'ment --option"
]

exports['utils crossArguments ignores end char (`) if not at the end of an argument 1'] = [
"start",
"8080",
"test argu`ment --option"
]

exports['utils crossArguments ignores end chars that are != the startChar of an argument 1'] = [
"start",
"8080",
"test argument' --option"
]

exports['utils getArguments allows 5 arguments 1'] = {
"args": [
"start",
@@ -11,13 +53,13 @@ exports['utils getArguments allows 5 arguments 1'] = {
{
"start": "npm run start",
"url": [
"http://localhost:6000"
"http://127.0.0.1:6000"
]
},
{
"start": "start:web",
"url": [
"http://localhost:6010"
"http://127.0.0.1:6010"
]
}
],
@@ -38,13 +80,13 @@ exports['utils getArguments determines NPM script for each command 1'] = {
{
"start": "npm run startA",
"url": [
"http://localhost:6000"
"http://127.0.0.1:6000"
]
},
{
"start": "npm run startB",
"url": [
"http://localhost:6010"
"http://127.0.0.1:6010"
]
}
],
@@ -75,7 +117,7 @@ exports['utils getArguments returns 3 arguments 1'] = {
{
"start": "npm run start",
"url": [
"http://localhost:8080"
"http://127.0.0.1:8080"
]
}
],
@@ -100,7 +142,7 @@ exports['utils getArguments understands custom commands 1'] = {
{
"start": "custom-command --with argument",
"url": [
"http://localhost:3000"
"http://127.0.0.1:3000"
]
}
],
@@ -112,9 +154,9 @@ exports['utils getArguments understands several ports 1'] = {
{
"start": "npm run start",
"url": [
"http://localhost:3000",
"http://localhost:4000",
"http://localhost:5000"
"http://127.0.0.1:3000",
"http://127.0.0.1:4000",
"http://127.0.0.1:5000"
]
}
],
@@ -126,7 +168,7 @@ exports['utils getArguments understands single :port 1'] = {
{
"start": "npm run start",
"url": [
"http://localhost:3000"
"http://127.0.0.1:3000"
]
}
],
@@ -138,7 +180,7 @@ exports['utils getArguments understands single port 1'] = {
{
"start": "npm run start",
"url": [
"http://localhost:3000"
"http://127.0.0.1:3000"
]
}
],
@@ -150,7 +192,7 @@ exports['utils getArguments understands start plus url 1'] = {
{
"start": "start-server",
"url": [
"http://localhost:6000"
"http://127.0.0.1:6000"
]
}
],
@@ -162,7 +204,7 @@ exports['utils getArguments understands url plus test 1'] = {
{
"start": "npm run start",
"url": [
"http://localhost:6000"
"http://127.0.0.1:6000"
]
}
],
50,701 changes: 35,780 additions & 14,921 deletions package-lock.json

Large diffs are not rendered by default.

31 changes: 23 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@
}
},
"engines": {
"node": ">=6"
"node": ">=16"
},
"files": [
"src/**/*.js",
@@ -48,7 +48,7 @@
"main": "src/",
"private": false,
"publishConfig": {
"registry": "http://registry.npmjs.org/"
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
@@ -73,11 +73,14 @@
"start-multiple": "node test/multiple-servers.js",
"start-https": "node test/https-server.js",
"start-fail": "node test/server-fail.js",
"start-304": "node test/server-304.js",
"start-403": "node test/server-403.js",
"start-cross-env": "cross-env FOO=bar node test/server.js",
"test2": "curl http://127.0.0.1:9000",
"test3": "curl http://127.0.0.1:9000 && curl http://127.0.0.1:9001",
"test4": "curl --insecure https://127.0.0.1:9000",
"message": "echo Hi there 👋",
"message2": "echo Hi there 2 👋",
"demo": "node src/bin/start.js http://127.0.0.1:9000 message",
"demo2": "node src/bin/start.js start http://127.0.0.1:9000 test2",
"demo3": "node src/bin/start.js start-with-child http://127.0.0.1:9000 test",
@@ -89,11 +92,22 @@
"demo9": "node src/bin/start.js start-https \"https://127.0.0.1:9000\" test4",
"demo10": "node src/bin/start.js start-fail http://127.0.0.1:9000 test",
"demo11": "node src/bin/start.js http-get://127.0.0.1:9000",
"demo12": "node src/bin/start.js start-304 9000 test2",
"demo-expect-403": "node src/bin/start.js --expect 403 start-403 9000 'echo Waited'",
"demo-interval": "WAIT_ON_INTERVAL=1000 node src/bin/start.js start http://127.0.0.1:9000 test2",
"demo-timeout": "WAIT_ON_TIMEOUT=10000 node src/bin/start.js start http://127.0.0.1:9000 test2",
"demo-cross-env": "node src/bin/start.js start-cross-env 9000",
"demo-commands": "node src/bin/start.js 'node test/server.js --port 8800' 8800 'node test/client --port 8800'",
"demo-multiple": "node src/bin/start.js 'node test/server --port 6000' 6000 'node test/server --port 6010' 6010 'curl http://127.0.0.1:6000 && curl http://127.0.0.1:6010'"
"demo-multiple": "node src/bin/start.js 'node test/server --port 6000' 6000 'node test/server --port 6010' 6010 'curl http://127.0.0.1:6000 && curl http://127.0.0.1:6010'",
"demo-multiple-test-commands": "node src/bin/start.js 9000 'npm run message && npm run message2'",
"demo-json-server": "WAIT_ON_TIMEOUT=10000 DEBUG=start-server-and-test node src/bin/start.js 'json-server test/data.json' localhost:3000 'echo json-server working'",
"demo-ip6": "WAIT_ON_TIMEOUT=10000 DEBUG=start-server-and-test node src/bin/start.js 'node test/ip6.mjs' localhost:8000 'echo server with ::1 working'",
"demo-zero": "WAIT_ON_TIMEOUT=10000 node src/bin/start.js 'node test/zero.mjs' 8000 'echo server with 0.0.0.0 working'",
"demo-zero-127": "WAIT_ON_TIMEOUT=10000 node src/bin/start.js 'node test/zero.mjs' http://127.0.0.1:8000 'echo server with 0.0.0.0 working'",
"demo-zero-explicit": "node src/bin/start.js 'node test/zero.mjs' http://0.0.0.0:8000 'echo server with 0.0.0.0 working'"
},
"devDependencies": {
"@types/node": "^18.14.1",
"ban-sensitive-files": "1.9.7",
"chai": "4.2.0",
"cross-env": "7.0.2",
@@ -102,6 +116,7 @@
"dont-crack": "1.2.1",
"git-issues": "1.3.1",
"got": "9.6.0",
"json-server": "^0.17.1",
"license-checker": "24.1.0",
"minimist": "1.2.5",
"mocha": "7.1.1",
@@ -112,17 +127,17 @@
"sinon": "9.0.2",
"sinon-chai": "3.5.0",
"snap-shot-it": "6.3.5",
"standard": "13.1.0",
"travis-deploy-once": "5.0.11"
"standard": "13.1.0"
},
"dependencies": {
"arg": "^5.0.2",
"bluebird": "3.7.2",
"check-more-types": "2.24.0",
"debug": "4.3.1",
"execa": "3.4.0",
"debug": "4.3.4",
"execa": "5.1.1",
"lazy-ass": "1.6.0",
"ps-tree": "1.2.0",
"wait-on": "5.2.1"
"wait-on": "7.2.0"
},
"release": {
"analyzeCommits": {
13 changes: 10 additions & 3 deletions src/bin/start.js
Original file line number Diff line number Diff line change
@@ -2,10 +2,13 @@

const debug = require('debug')('start-server-and-test')

const args = process.argv.slice(2)
const startAndTest = require('..').startAndTest
const utils = require('../utils')

const namedArguments = utils.getNamedArguments(process.argv.slice(2))
debug('named arguments: %o', namedArguments)

const args = utils.crossArguments(process.argv.slice(2))
debug('parsing CLI arguments: %o', args)
const parsed = utils.getArguments(args)
debug('parsed args: %o', parsed)
@@ -15,9 +18,13 @@ if (!Array.isArray(services)) {
throw new Error(`Could not parse arguments %o, got %o`, args, parsed)
}

utils.printArguments({ services, test })
if (!namedArguments.expect) {
namedArguments.expect = 200
}

utils.printArguments({ services, test, namedArguments })

startAndTest({ services, test }).catch(e => {
startAndTest({ services, test, namedArguments }).catch(e => {
console.error(e)
process.exit(1)
})
37 changes: 30 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -13,27 +13,41 @@ const debug = require('debug')('start-server-and-test')
* Used for timeout (ms)
*/
const fiveMinutes = 5 * 60 * 1000
const twoSeconds = 2000

const waitOnTimeout = process.env.WAIT_ON_TIMEOUT
? Number(process.env.WAIT_ON_TIMEOUT)
: fiveMinutes

const waitOnInterval = process.env.WAIT_ON_INTERVAL
? Number(process.env.WAIT_ON_INTERVAL)
: twoSeconds

const isDebug = () =>
process.env.DEBUG && process.env.DEBUG.indexOf('start-server-and-test') !== -1

const isInsecure = () => process.env.START_SERVER_AND_TEST_INSECURE

function waitAndRun ({ start, url, runFn }) {
function waitAndRun ({ start, url, runFn, namedArguments }) {
la(is.unemptyString(start), 'missing start script name', start)
la(is.fn(runFn), 'missing test script name', runFn)
la(
is.unemptyString(url) || is.unemptyArray(url),
'missing url to wait on',
url
)
const isSuccessfulHttpCode = status =>
(status >= 200 && status < 300) || status === 304
const validateStatus = namedArguments.expect
? status => status === namedArguments.expect
: isSuccessfulHttpCode

debug('starting server with command "%s", verbose mode?', start, isDebug())

const server = execa(start, { shell: true, stdio: 'inherit' })
const server = execa(start, {
shell: true,
stdio: ['ignore', 'inherit', 'inherit']
})
let serverStopped

function stopServer () {
@@ -74,15 +88,16 @@ function waitAndRun ({ start, url, runFn }) {
debug('starting waitOn %s', url)
const options = {
resources: Array.isArray(url) ? url : [url],
interval: 2000,
interval: waitOnInterval,
window: 1000,
timeout: waitOnTimeout,
verbose: isDebug(),
strictSSL: !isInsecure(),
log: isDebug(),
headers: {
'Accept': 'text/html, application/json, text/plain, */*'
}
Accept: 'text/html, application/json, text/plain, */*'
},
validateStatus
}
debug('wait-on options %o', options)

@@ -113,27 +128,35 @@ const runTheTests = testCommand => () => {
* Starts a single service and runs tests or recursively
* runs a service, then goes to the next list, until it reaches 1 service and runs test.
*/
function startAndTest ({ services, test }) {
function startAndTest ({ services, test, namedArguments }) {
if (services.length === 0) {
throw new Error('Got zero services to start ...')
}

la(
is.number(namedArguments.expect),
'expected status should be a number',
namedArguments.expect
)

if (services.length === 1) {
const runTests = runTheTests(test)
debug('single service "%s" to run and test', services[0].start)
return waitAndRun({
start: services[0].start,
url: services[0].url,
namedArguments,
runFn: runTests
})
}

return waitAndRun({
start: services[0].start,
url: services[0].url,
namedArguments,
runFn: () => {
debug('previous service started, now going to the next one')
return startAndTest({ services: services.slice(1), test })
return startAndTest({ services: services.slice(1), test, namedArguments })
}
})
}
56 changes: 52 additions & 4 deletions src/utils-spec.js
Original file line number Diff line number Diff line change
@@ -26,6 +26,39 @@ describe('utils', () => {
})
})

context('crossArguments', () => {
const crossArguments = utils.crossArguments
;['"', "'", '`'].forEach(char => {
it(`concates arguments if wrapped by ${char}`, () => {
snapshot(
crossArguments([
'start',
'8080',
`${char}test`,
'argument',
`--option${char}`
])
)
})
it(`ignores end char (${char}) if not at the end of an argument`, () => {
snapshot(
crossArguments([
'start',
'8080',
`${char}test`,
`argu${char}ment`,
`--option${char}`
])
)
})
})
it(`ignores end chars that are != the startChar of an argument`, () => {
snapshot(
crossArguments(['start', '8080', `"test`, `argument'`, `--option"`])
)
})
})

context('getArguments', () => {
const getArguments = utils.getArguments

@@ -168,12 +201,27 @@ describe('utils', () => {
})

it('changes port to localhost', () => {
la(arrayEq(normalizeUrl('6006'), ['http://localhost:6006']))
la(arrayEq(normalizeUrl(8080), ['http://localhost:8080']))
la(arrayEq(normalizeUrl('6006'), ['http://127.0.0.1:6006']))
la(arrayEq(normalizeUrl(8080), ['http://127.0.0.1:8080']))
})

it('changes :port to localhost', () => {
la(arrayEq(normalizeUrl(':6006'), ['http://localhost:6006']))
la(arrayEq(normalizeUrl(':6006'), ['http://127.0.0.1:6006']))
})

it('appends http to localhost', () => {
la(arrayEq(normalizeUrl('localhost'), ['http://localhost']))
la(arrayEq(normalizeUrl('localhost:3030'), ['http://localhost:3030']))
})

it('appends http to 127.0.0.1', () => {
la(arrayEq(normalizeUrl('127.0.0.1'), ['http://127.0.0.1']))
la(arrayEq(normalizeUrl('127.0.0.1:3030'), ['http://127.0.0.1:3030']))
})

it('appends http to 0.0.0.0', () => {
la(arrayEq(normalizeUrl('0.0.0.0'), ['http://0.0.0.0']))
la(arrayEq(normalizeUrl('0.0.0.0:3030'), ['http://0.0.0.0:3030']))
})

it('returns original argument if does not know what to do', () => {
@@ -184,7 +232,7 @@ describe('utils', () => {
it('parses multiple resources', () => {
la(
arrayEq(normalizeUrl(':6006|http://foo.com'), [
'http://localhost:6006',
'http://127.0.0.1:6006',
'http://foo.com'
])
)
113 changes: 107 additions & 6 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,73 @@ const la = require('lazy-ass')
const is = require('check-more-types')
const { join } = require('path')
const { existsSync } = require('fs')
const arg = require('arg')
const debug = require('debug')('start-server-and-test')

const namedArguments = {
'--expect': Number
}

/**
* Returns new array of command line arguments
* where leading and trailing " and ' are indicating
* the beginning and end of an argument.
*/
const crossArguments = cliArguments => {
const args = arg(namedArguments, {
permissive: true,
argv: cliArguments
})
debug('initial parsed arguments %o', args)
// all other arguments
const cliArgs = args._

let concatModeChar = false
const indicationChars = ["'", '"', '`']
const combinedArgs = []
for (let i = 0; i < cliArgs.length; i++) {
let arg = cliArgs[i]
if (
!concatModeChar &&
indicationChars.some(char => cliArgs[i].startsWith(char))
) {
arg = arg.slice(1)
}
if (concatModeChar && cliArgs[i].endsWith(concatModeChar)) {
arg = arg.slice(0, -1)
}

if (concatModeChar && combinedArgs.length) {
combinedArgs[combinedArgs.length - 1] += ' ' + arg
} else {
combinedArgs.push(arg)
}

if (
!concatModeChar &&
indicationChars.some(char => cliArgs[i].startsWith(char))
) {
concatModeChar = cliArgs[i][0]
}
if (concatModeChar && cliArgs[i].endsWith(concatModeChar)) {
concatModeChar = false
}
}
return combinedArgs
}

const getNamedArguments = cliArgs => {
const args = arg(namedArguments, {
permissive: true,
argv: cliArgs
})
debug('initial parsed arguments %o', args)
return {
expect: args['--expect'],
// aliases
'--expected': '--expect'
}
}

/**
* Returns parsed command line arguments.
@@ -122,51 +189,85 @@ const isUrlOrPort = input => {
})
}

/**
* Returns the host to ping if the user specified just the port.
* For a long time, the safest bet was "localhost", but now modern
* web servers seem to bind to "0.0.0.0", which means
* the "127.0.0.1" works better
*/
const getHost = () => '127.0.0.1'

const normalizeUrl = input => {
const str = is.string(input) ? input.split('|') : [input]
const defaultHost = getHost()

return str.map(s => {
if (is.url(s)) {
return s
}

if (is.number(s) && is.port(s)) {
return `http://localhost:${s}`
return `http://${defaultHost}:${s}`
}

if (!is.string(s)) {
return s
}

if (
s.startsWith('localhost') ||
s.startsWith('127.0.0.1') ||
s.startsWith('0.0.0.0')
) {
return `http://${s}`
}

if (is.port(parseInt(s))) {
return `http://localhost:${s}`
return `http://${defaultHost}:${s}`
}

if (s[0] === ':') {
return `http://localhost${s}`
return `http://${defaultHost}${s}`
}
// for anything else, return original argument
return s
})
}

function printArguments ({ services, test }) {
function printArguments ({ services, test, namedArguments }) {
la(
is.number(namedArguments.expect),
'expected status code should be a number',
namedArguments.expect
)

services.forEach((service, k) => {
console.log('%d: starting server using command "%s"', k + 1, service.start)
console.log(
'and when url "%s" is responding with HTTP status code 200',
service.url
'and when url "%s" is responding with HTTP status code %d',
service.url,
namedArguments.expect
)
})

if (process.env.WAIT_ON_INTERVAL !== undefined) {
console.log('WAIT_ON_INTERVAL is set to', process.env.WAIT_ON_INTERVAL)
}

if (process.env.WAIT_ON_TIMEOUT !== undefined) {
console.log('WAIT_ON_TIMEOUT is set to', process.env.WAIT_ON_TIMEOUT)
}

console.log('running tests using command "%s"', test)
console.log('')
}

// placing functions into a common object
// makes them methods for easy stubbing
const UTILS = {
crossArguments,
getArguments,
getNamedArguments,
isPackageScriptName,
isUrlOrPort,
normalizeUrl,
3 changes: 3 additions & 0 deletions test/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"todos": []
}
17 changes: 17 additions & 0 deletions test/ip6.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import http from 'node:http';

// Create a local server to receive data from
const server = http.createServer();

// Listen to the request event
server.on('request', (request, res) => {
console.log('server responding')
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});

server.listen(8000, '::1', () => {
console.log('server is listening on ::1:8000')
});
20 changes: 20 additions & 0 deletions test/server-304.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const argv = require('minimist')(process.argv.slice(2), {
alias: {
port: 'p'
}
})
const http = require('http')
const server = http.createServer((req, res) => {
console.log(req.method)
if (req.method === 'GET') {
res.writeHead(304).end('All good\n\n')
} else {
res.end()
}
})
const port = argv.port || 9000
setTimeout(() => {
server.listen(port)
console.log('listening at port %d', port)
}, 5000)
console.log('sleeping for 5 seconds before starting')
20 changes: 20 additions & 0 deletions test/server-403.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const argv = require('minimist')(process.argv.slice(2), {
alias: {
port: 'p'
}
})
const http = require('http')
const server = http.createServer((req, res) => {
if (req.method === 'GET' || req.method === 'HEAD') {
console.log('%s returning 403', req.method)
res.writeHead(403).end('Unauthorized\n\n')
} else {
res.end()
}
})
const port = argv.port || 9000
setTimeout(() => {
server.listen(port)
console.log('listening at port %d, responding with 403', port)
}, 5000)
console.log('sleeping for 5 seconds before starting')
17 changes: 17 additions & 0 deletions test/zero.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import http from 'node:http';

// Create a local server to receive data from
const server = http.createServer();

// Listen to the request event
server.on('request', (request, res) => {
console.log('server responding')
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!',
}));
});

server.listen(8000, '0.0.0.0', () => {
console.log('server is listening on 0.0.0.0:8000')
});