mirror of
https://github.com/amnezia-vpn/openvpn3.git
synced 2026-05-17 00:16:12 +03:00
Improvde Markdown documents
- Fix header nesting in webauth.md. - Remove trailing whitespaces. - Fix typo in unittests document. Signed-off-by: Frank Lichtenheld <frank@lichtenheld.com>
This commit is contained in:
223
doc/webauth.md
223
doc/webauth.md
@@ -1,92 +1,86 @@
|
||||
Contents of this documents
|
||||
==========================
|
||||
This document describes various web based protocols that are used together with OpenVPN.
|
||||
# OpenVPN-related Web protocols #
|
||||
|
||||
This document describes various web based protocols that are used together with OpenVPN.
|
||||
They are not part of core protocol of OpenVPN but are extremely closely related.
|
||||
Interoperability between products building on OpenVPN is expected, so products
|
||||
are expect to implement web based mechanisms according to this specification.
|
||||
|
||||
This document has a three parts: The first focuses web based authentication during
|
||||
connection (AUTH_PENDING). The second describes standardised
|
||||
endpoints for client applications to retrieve a client profile. And final third section
|
||||
connection (AUTH_PENDING). The second describes standardised
|
||||
endpoints for client applications to retrieve a client profile. And final third section
|
||||
describes a simpler REST based interface that OpenVPN Connect Client and Access Server use to
|
||||
download profiles to the client with minimal user interaction.
|
||||
|
||||
OpenVPN web auth protocol during Connect
|
||||
========================================
|
||||
## OpenVPN web auth protocol during Connect ##
|
||||
|
||||
This document describes the assumption and what client and server should implement
|
||||
to facilitate a web based second factor login for OpenVPN.
|
||||
|
||||
Triggering web based authentication
|
||||
-----------------------------------
|
||||
### Triggering web based authentication ###
|
||||
|
||||
To trigger web based authentication the client needs to signal its ability with `IV_SSO` and
|
||||
the server needs to send the url to the client. The details are documented in
|
||||
https://github.com/OpenVPN/openvpn/blob/master/doc/management-notes.txt
|
||||
and are outside the scope of this document.
|
||||
|
||||
Receiving a WEB_AUTH/OPEN_URL request
|
||||
------------------------------------
|
||||
### Receiving a WEB_AUTH/OPEN_URL request ###
|
||||
|
||||
When the client receives an `WEB_AUTH` or `OPEN_URL` (deprecated) request to continue the
|
||||
authentication via web based authentication the client should directly open the web page or
|
||||
prompt the user to open the web page. This can be either can in an internal browser windows/webview
|
||||
When the client receives an `WEB_AUTH` or `OPEN_URL` (deprecated) request to continue the
|
||||
authentication via web based authentication the client should directly open the web page or
|
||||
prompt the user to open the web page. This can be either can in an internal browser windows/webview
|
||||
or open the web page in an external browser. A web based login should be able to handle both cases.
|
||||
|
||||
There is a special "initially hidden" mode that is explained in the internal webview section.
|
||||
|
||||
Auth-token usage
|
||||
----------------
|
||||
### Auth-token usage ###
|
||||
|
||||
The server side should try to minimize the number of web based authentication requests
|
||||
to the client. This helps avoid issues when the client cannot reach the authentication
|
||||
portal during reconnect in addition to avoid disturbing the user with authentication
|
||||
requests in the web views. This disturbance will be even more annoying when an external
|
||||
to the client. This helps avoid issues when the client cannot reach the authentication
|
||||
portal during reconnect in addition to avoid disturbing the user with authentication
|
||||
requests in the web views. This disturbance will be even more annoying when an external
|
||||
browser window is opened by a client app not having the user's focus.
|
||||
|
||||
To avoid this, the server should send an authentication token to the client
|
||||
(see `--auth-token` and `--auth-gen-token` in the OpenVPN man page as well as
|
||||
To avoid this, the server should send an authentication token to the client
|
||||
(see `--auth-token` and `--auth-gen-token` in the OpenVPN man page as well as
|
||||
`doc/management-notes.txt`). When the server sends an authentication token to the client,
|
||||
the client will use that token instead of normal user credentials for the succeeding
|
||||
the client will use that token instead of normal user credentials for the succeeding
|
||||
authentication requests, for as long as the token is considered valid by the server.
|
||||
|
||||
Internal webview integration API
|
||||
--------------------------------
|
||||
### Internal webview integration API ###
|
||||
|
||||
When the application uses an internal browser for web login process the API
|
||||
allows tighter integration of the login. The application should append a
|
||||
`embedded=true` parameter to the URL provided by the server to indicate the request
|
||||
is made from an internal webview. The server may choose to serve a web page that
|
||||
integrates better into the flow of an app or ignore the parameter.
|
||||
is made from an internal webview. The server may choose to serve a web page that
|
||||
integrates better into the flow of an app or ignore the parameter.
|
||||
|
||||
If an internal webview is used, the app should clearly indicate that the user is interacting
|
||||
If an internal webview is used, the app should clearly indicate that the user is interacting
|
||||
with an external website rather than with the app itself to avoid phishing attacks.
|
||||
|
||||
Initial hiding of a webview
|
||||
---------------------------
|
||||
### Initial hiding of a webview ###
|
||||
|
||||
There are situations where initially hiding the webview is desirable. Mainly when relying
|
||||
on persistent storage/cookies on the client webview to determine if user input is required
|
||||
or not. Starting all web auth hidden would break web login pages that are not specifically
|
||||
designed to work with OpenVPN. Therefore, this feature must be implemented strictly as opt-in.
|
||||
To enable this mode the flags of WEB_AUTH need to contain `hidden`.
|
||||
the url. When a client implements this mode it must also implement the State API to allow
|
||||
the web page to show the webview. A web login must not depend on the feature being present.
|
||||
To enable this mode the flags of WEB_AUTH need to contain `hidden`.
|
||||
the url. When a client implements this mode it must also implement the State API to allow
|
||||
the web page to show the webview. A web login must not depend on the feature being present.
|
||||
|
||||
### State API ###
|
||||
|
||||
State API
|
||||
---------
|
||||
This API is optional on both server and client side. Neither should the client side assume
|
||||
that this API will be used during login process nor should the server side assume
|
||||
that this API is present.
|
||||
that this API will be used during login process nor should the server side assume
|
||||
that this API is present.
|
||||
|
||||
The reason to make this API optional on the client side is that client are allowed to open
|
||||
the URL in the default browser of the user that has no specific OpenVPN support. On the server
|
||||
side any identity provider should be able to be used. Unless the IdP is tightly integrated/designed
|
||||
for OpenVPN web auth, it will not support the State API.
|
||||
for OpenVPN web auth, it will not support the State API.
|
||||
|
||||
To communicate the current state of the web based login with an application that
|
||||
implements an internal webview, the web page should use a JavaScript mechanism based on
|
||||
[postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
|
||||
To communicate the current state of the web based login with an application that
|
||||
implements an internal webview, the web page should use a JavaScript mechanism based on
|
||||
[postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
|
||||
|
||||
The wep page should send events with either appEvent.postMessage if appEvent exists and
|
||||
window.parent.postMessage otherwise. The data is a JSON dictionary with `type` and an optional
|
||||
@@ -95,42 +89,42 @@ window.parent.postMessage otherwise. The data is a JSON dictionary with `type` a
|
||||
appEvent.postMessage({"type": <event_string>, "data": <any>})
|
||||
|
||||
The approach of first trying to use `appEvent` and otherwise `window.parent` is to maximise
|
||||
compatibility with various webview implementations.
|
||||
|
||||
compatibility with various webview implementations.
|
||||
|
||||
The events that are defined are:
|
||||
|
||||
For events where `data` is not defined, the `data` field is reserved and a client should ignore
|
||||
it when present.
|
||||
|
||||
- `ACTION_REQUIRED`: This signal to the client that a user input is required.
|
||||
- `ACTION_REQUIRED`: This signal to the client that a user input is required.
|
||||
|
||||
If the webview is currently hidden, the webview needs to be shown to the user.
|
||||
|
||||
If the webview is currently hidden, the webview needs to be shown to the user.
|
||||
|
||||
This event can be ignored if the client does not implement the hidden initial webview.
|
||||
|
||||
- `CONNECT_SUCCESS` and `CONNECT_FAILED`: The web login process was successful/failed. For the logic of the
|
||||
application this state is just informal and can be shown in the VPN connection progress. The real
|
||||
|
||||
- `CONNECT_SUCCESS` and `CONNECT_FAILED`: The web login process was successful/failed. For the logic of the
|
||||
application this state is just informal and can be shown in the VPN connection progress. The real
|
||||
success/failure condition is determined by the VPN connection succeeding/failing.
|
||||
|
||||
|
||||
The application should close the webview on receiving this event.
|
||||
|
||||
|
||||
- `LOCATION_CHANGE`: This notifies the internal webview to change the title to the provided title as
|
||||
string in `data`, for example
|
||||
|
||||
|
||||
{"type": "LOCATION_CHANGE", "data": "Flower power VPN login step 2/3"}
|
||||
|
||||
|
||||
Web pages also need to implement the traditional changing of the web page to change the title when
|
||||
the page is opening in an external browser.
|
||||
|
||||
|
||||
Certificate checks
|
||||
------------------
|
||||
A client must implement certificate checks. It is recommended to implement a user dialog
|
||||
|
||||
|
||||
### Certificate checks ###
|
||||
|
||||
A client must implement certificate checks. It is recommended to implement a user dialog
|
||||
step where the user is presented the subject information of an unknown certificate, which
|
||||
the user can choose to accept or reject.
|
||||
|
||||
The client profile may also contain an embedded custom CA certificate. These CA certificates
|
||||
should be considered trustworthy without interaction from the user. The embedded custom
|
||||
should be considered trustworthy without interaction from the user. The embedded custom
|
||||
certificate are included in the client profile like this:
|
||||
|
||||
# OVPN_ACCESS_SERVER_WEB_CA_BUNDLE_START
|
||||
@@ -139,83 +133,78 @@ certificate are included in the client profile like this:
|
||||
# -----END CERTIFICATE-----
|
||||
# OVPN_ACCESS_SERVER_WEB_CA_BUNDLE_STOP
|
||||
|
||||
Profile download
|
||||
================
|
||||
## Profile download ##
|
||||
|
||||
There are two possible ways users may be provided client profiles: Browser based download
|
||||
interface and a generic direct download interface. The browser based interface is best suited
|
||||
for implementations providing an embedded browser experience, where the app can pick up the
|
||||
downloaded file and parse it further. The browser based interface is also useful when the server
|
||||
can provide different client profiles for various server regions. The generic direct download API
|
||||
for implementations providing an embedded browser experience, where the app can pick up the
|
||||
downloaded file and parse it further. The browser based interface is also useful when the server
|
||||
can provide different client profiles for various server regions. The generic direct download API
|
||||
will not require any user interaction and the received client profile can be parsed instantly.
|
||||
|
||||
|
||||
Web based profile download
|
||||
==========================
|
||||
|
||||
### Web based profile download ###
|
||||
|
||||
This API is intended to provide a uniform way for client to initiate the download of a profile
|
||||
and have the login/download process performed through a webview. For an internal webview
|
||||
the state API provides event to automatically trigger the import of a profile. If an external
|
||||
browser the OpenVPN profile should be offered as download with be content-type
|
||||
the state API provides event to automatically trigger the import of a profile. If an external
|
||||
browser the OpenVPN profile should be offered as download with be content-type
|
||||
`application/x-openvpn-profile`.
|
||||
|
||||
Detection of web based profile download support
|
||||
-----------------------------------------------
|
||||
|
||||
#### Detection of web based profile download support ####
|
||||
|
||||
The classic way of a downloading profiles is outlined in the next section. To determine
|
||||
what method has to be used to download a profile from a server, the client should do
|
||||
what method has to be used to download a profile from a server, the client should do
|
||||
a `HEAD` or `GET` request to `https://servername/openvpn-api/profile`. If the response
|
||||
contains a header `Ovpn-WebAuth` with any value, the web based method should be used.
|
||||
|
||||
The header `Ovpn-WebAuth` has the following format:
|
||||
|
||||
Ovpn-WebAuth: providername,flags
|
||||
|
||||
|
||||
The flags are also comma separated values. Currently, the followings flag that are defined:
|
||||
|
||||
|
||||
* hidden-webview Starts the webview in hidden mode. See the web auth section for more details
|
||||
* external Indicates that an internal webivew should NOT be used but instead a normal
|
||||
browser is to be used.
|
||||
* internal Indicates that the internal webview should be used if possible
|
||||
* internal Indicates that the internal webview should be used if possible
|
||||
|
||||
In general websites should also report ovpn-webauth without `embedded=true` parameter to allow
|
||||
clients without internal browser support to craft a url to open in an external browser that
|
||||
contains the additional parameters like `deviceID` and indicates `tls-cryptv2` support.
|
||||
|
||||
To start the web based method the client should load the url
|
||||
|
||||
|
||||
https://servername/openvpn-api/profile
|
||||
|
||||
|
||||
with the following optional parameters:
|
||||
|
||||
- `deviceID` unique device ID, must be identical with the ID provided with `IV_HWADDR` if provided
|
||||
- `deviceID` unique device ID, must be identical with the ID provided with `IV_HWADDR` if provided
|
||||
- `deviceModel` model of connected device
|
||||
- `deviceOS` OS version of connected device
|
||||
- `appVersion` Version of OpenVPN client
|
||||
- `appVersion` Version of OpenVPN client
|
||||
- `embedded` Request is made from an internal webview. The server may choose to serve a
|
||||
web page that integrates better into the flow of an app or ignore the parameter.
|
||||
- `tls-cryptv2` Should be set to 1 if the client supports tls-crypt-v2
|
||||
- `auth=method` The app supports the auth specified AUTH_PENDING authentication method. The parameter
|
||||
can be specified multiple times. The values for the method parameter
|
||||
are the same as in the `IV_SSO` parameter. For example an app supporting text based
|
||||
challenge response and the web based authentication would add
|
||||
challenge response and the web based authentication would add
|
||||
`auth=crtext&auth=openurl` to the request.
|
||||
|
||||
Optional web based profile support
|
||||
----------------------------------
|
||||
#### Optional web based profile support ####
|
||||
|
||||
In certain scenarios the web-based and the Rest API based profile import might be mixed.
|
||||
In these cases the client should offer the Rest API based import by default but also offer
|
||||
some way of switching to the web based import flow.
|
||||
In these cases the client should offer the Rest API based import by default but also offer
|
||||
some way of switching to the web based import flow.
|
||||
|
||||
The server will indicate the optional web based import by adding the `Ovpn-WebAuth-Optional`
|
||||
The server will indicate the optional web based import by adding the `Ovpn-WebAuth-Optional`
|
||||
header with the same format as the `Ovpn-Webauth` header instead of the `Ovpn-WebAuth` header.
|
||||
The header has the same format as the `Ovpn-WebAuth` header:
|
||||
|
||||
Ovpn-WebAuth-Optional: providername,flags
|
||||
|
||||
In addition to the flags specified for `Ovpn-WebAuth`, the `Ovpn-WebAuth-Optional` can have
|
||||
In addition to the flags specified for `Ovpn-WebAuth`, the `Ovpn-WebAuth-Optional` can have
|
||||
one additional optional flag:
|
||||
|
||||
* name=description
|
||||
@@ -223,45 +212,42 @@ one additional optional flag:
|
||||
to allow the UI to show the name of the web import to give the user a hint when to use the
|
||||
web based import in lieu of the simple username/password based REST import. As an example
|
||||
the `Ovpn-WebAuth-Optional` header might look like:
|
||||
|
||||
|
||||
Ovpn-WebAuth-Optional: FlowerVPN,name=Smartcard SAML authentication,external
|
||||
|
||||
The normal user/password based VPN import dialog for the Rest-API would then show an
|
||||
additional button/link that says:
|
||||
|
||||
Import profile using "Smartcard SAML authentication"
|
||||
additional button/link that says:
|
||||
|
||||
State API
|
||||
---------
|
||||
Import profile using "Smartcard SAML authentication"
|
||||
|
||||
#### State API ####
|
||||
|
||||
The state API is identical to the API used during connection. The web page can
|
||||
also use the `LOCATION_CHANGED` event and additionally should provide the following
|
||||
events.
|
||||
|
||||
- `PROFILE_DOWNLOAD_FAILED`: The process in the web page to download the profile was unsuccessful.
|
||||
- `PROFILE_DOWNLOAD_FAILED`: The process in the web page to download the profile was unsuccessful.
|
||||
The client should close the webview.
|
||||
- `PROFILE_DOWNLOAD_SUCCESS`: Download a of profile worked. The client should import the profile
|
||||
in the next step. `data` will contain the profile as json object with `profile` and `title` as
|
||||
keys. `profile` will contain the ovpn profile and title will have a suggested title
|
||||
for the new profile:
|
||||
|
||||
|
||||
{"type": "PROFILE_DOWNLOAD_SUCCESS", "data": {"profile": "<.ovpn profile>", "title": "<title>"}}
|
||||
|
||||
|
||||
To allow a client to connect directly after downloading a profile without requiring a
|
||||
web authentication on the first the connect, the two optional keys `vpn-session-user`
|
||||
and `vpn-session-token` can be present that act as `the auth-token` to be used on
|
||||
the first connect. These keys are not base64 encoded (unlike in the REST based download)
|
||||
since JSON has proper escaping:
|
||||
|
||||
|
||||
{"type": "PROFILE_DOWNLOAD_SUCCESS", "data": {"profile": "<.ovpn profile content>", "vpn-session-user": "foo\'bar", "vpn-session-token": "AT-123456789"}}
|
||||
|
||||
The implementation of `vpn-session-token` and `vpn-session-user` is optional but strongly
|
||||
recommended to improve user experience.
|
||||
|
||||
### openvpn://import-profile URL ###
|
||||
|
||||
|
||||
openvpn://import-profile URL
|
||||
============================
|
||||
To trigger a direct import of a profile in an OpenVPN app an openvpn://import-profile/
|
||||
link can be used. The syntax of the link is
|
||||
|
||||
@@ -278,32 +264,32 @@ The API MUST provide an openvpn profile with the MIME-type of `application/x-ope
|
||||
Any other response is invalid. The response MAY contain the `VPN-Session-User` and
|
||||
`VPN-Session-Token` headers as described in the Basic API section.
|
||||
|
||||
|
||||
REST profile download API
|
||||
=========================
|
||||
|
||||
## REST profile download API ##
|
||||
|
||||
REST is a simple and more lightweight interface to download profiles.
|
||||
This is currently mainly implemented by OpenVPN Access Server API and
|
||||
This is currently mainly implemented by OpenVPN Access Server API and
|
||||
Connect clients but can also be implemented by other server and clients.
|
||||
The endpoint is https://servername/rest/methodname
|
||||
|
||||
Basic API
|
||||
-----------
|
||||
### Basic API ###
|
||||
|
||||
Access server calls profile that require username and password Userlogin
|
||||
profile and profile without Autologin. This is also replicated in the
|
||||
method name (`rest/GetAutologin` or `rest/GetUserlogin`) in the request URL.
|
||||
method name (`rest/GetAutologin` or `rest/GetUserlogin`) in the request URL.
|
||||
The configuration file is returned as a `text/plain` HTTP document (Note: this
|
||||
in contrast to the right type `application/x-openvpn-profile`). Credentials
|
||||
in contrast to the right type `application/x-openvpn-profile`). Credentials
|
||||
are specified using HTTP Basic Authentication. The REST API is implemented
|
||||
through an SSL web server. The client must do the normal SSL certificate check
|
||||
but should also allow a user to pin a self-signed certificate.
|
||||
|
||||
The client should also indicate support supported features that influence
|
||||
profile generation and cannot be negotiated by the VPN protocol.
|
||||
profile generation and cannot be negotiated by the VPN protocol.
|
||||
Currently only TLS Crypt V2 support indicated by adding a tls-cryptv2=1
|
||||
to the request
|
||||
|
||||
Typically a client app will present a username and password input field and
|
||||
checkbox to enable/disable autologin.
|
||||
checkbox to enable/disable autologin.
|
||||
|
||||
To get the Autologin configuration using curl (from a client without tls-cryptv2 support):
|
||||
|
||||
@@ -319,14 +305,13 @@ double 2FA login in a short amount of time:
|
||||
|
||||
VPN-Session-User: base64_encoded_user
|
||||
VPN-Session-Token: base64_encoded_pw
|
||||
|
||||
These parameters are optional but should be used as VPN session user and token when
|
||||
|
||||
These parameters are optional but should be used as VPN session user and token when
|
||||
initiating a connection directly after downloading a profile.
|
||||
|
||||
Error reporting with Rest API
|
||||
-----------------------------
|
||||
### Error reporting with Rest API ###
|
||||
|
||||
Internally OpenVPN Access server used XMLRPC when this and the current
|
||||
Internally OpenVPN Access server used XMLRPC when this and the current
|
||||
implementation did not properly account for this, so error reporting is
|
||||
done with xml replies. This is an unfortunate design/implementation detail.
|
||||
So the rest error replies look more like XMLRPC errors than rest error.
|
||||
@@ -359,10 +344,10 @@ User is not enrolled through the WEB client yet:
|
||||
<Message>You must enroll this user in Authenticator first before you are allowed to retrieve a connection profile. (9008)</Message>
|
||||
</Error>
|
||||
|
||||
Webauth fallback
|
||||
----------------
|
||||
### Webauth fallback ###
|
||||
|
||||
This is used when the server is configured to use username/password as general
|
||||
authentication method but some users are setup to used the web based
|
||||
authentication method but some users are setup to used the web based
|
||||
authentication method. Should a user that requires web based try to authenticate
|
||||
instead it will report an error:
|
||||
|
||||
@@ -377,8 +362,8 @@ detection of web based profile download. If the client encounters this error it
|
||||
should offer the user to continue to the import using the web based profile
|
||||
download method.
|
||||
|
||||
Challenge/response authentication
|
||||
---------------------------------
|
||||
### Challenge/response authentication ###
|
||||
|
||||
The challenge/response protocol for the Rest web api mirrors the approach
|
||||
taken by the old (non using AUTH-PENDING,cr-response) challenge/response
|
||||
of the OpenVPN protocol.
|
||||
|
||||
@@ -11,7 +11,7 @@ The unit test cmake files assume here that the deps directory is on the same
|
||||
level as the openvpn3 directory unless overridden by the DEP_DIR variable.
|
||||
|
||||
The directory for cmake to build a project can be everywhere, but it is recommended to keep
|
||||
it outside of the source tree.
|
||||
it outside of the source tree.
|
||||
|
||||
Building unit tests (assuming you are in the openvpn3 directory):
|
||||
|
||||
@@ -43,27 +43,27 @@ Examplary commands for building and running on Windows:
|
||||
➜ cmake -DDEP_DIR=C:\o3\deps -DUSE_MBEDTLS=true -DCMAKE_GENERATOR_PLATFORM=x64 C:\o3\openvpn3
|
||||
➜ cmake --build . --target coreUnitTests
|
||||
➜ test\unittests\Debug\coreUnitTests.exe --gtest_output="xml:test_core.xml" --gtest_shuffle
|
||||
|
||||
|
||||
### Frequently used command line options ###
|
||||
|
||||
Show the help for gtest command line options:
|
||||
|
||||
|
||||
➜ ./test/unittests/coreUnitTests --help
|
||||
|
||||
|
||||
Run only tests starting with Base64 or a sepcific Base64 test:
|
||||
|
||||
➜ ./test/unittests/coreUnitTests --gtest_filter='Base64.*'
|
||||
➜ ./test/unittests/coreUnitTests --gtest_filter=Base64.tooshortdest
|
||||
|
||||
|
||||
Run all test but the Base64 tests
|
||||
|
||||
➜ ./test/unittests/coreUnitTests --gtest_filter='-Base64.*'
|
||||
|
||||
|
||||
Multiple pattern can be specified with a list separated by :
|
||||
|
||||
➜ ./test/unittests/coreUnitTests --gtest_filter='OpenSSL_X509_get_serial.*:Base64.*'
|
||||
|
||||
|
||||
|
||||
|
||||
Shuffle order the order in which the tests are run:
|
||||
|
||||
➜ ./test/unittests/coreUnitTests --gtest_shuffle
|
||||
@@ -71,12 +71,12 @@ Shuffle order the order in which the tests are run:
|
||||
If a certain order yields failures, repeat that order:
|
||||
|
||||
➜ ./test/unittests/coreUnitTests --gtest_shuffle --gtest_random_seed=23
|
||||
|
||||
|
||||
Run also the tests that are normally disabled
|
||||
|
||||
➜ ./test/unittests/coreUnitTests --gtest_also_run_disabled_tests
|
||||
|
||||
## Writing unit tetss ##
|
||||
## Writing unit tests ##
|
||||
|
||||
Each new test suite should be a new a file called `test_suitename.cpp` and added to the
|
||||
`CMakeLists.txt` file. Each test includes an `#include test_common.h` at the top to setup
|
||||
|
||||
Reference in New Issue
Block a user