Skip to content

Commit

Permalink
Merge pull request #31 from jmg1138/more-tests
Browse files Browse the repository at this point in the history
More unit tests
  • Loading branch information
jmg1138 authored Dec 11, 2021
2 parents 6dda868 + 854e117 commit 04ea922
Show file tree
Hide file tree
Showing 16 changed files with 574 additions and 323 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ jobs:
- name: Build
run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true
- name: Test
run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
- name: Codecov
uses: codecov/codecov-action@v1
run: dotnet test --filter TestType!=End2End --no-build --verbosity quiet

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ test:
@# This will execute unit tests.
# ----------
# Running unit tests.
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
dotnet test --verbosity quiet /p:CollectCoverage=true /p:CoverletOutputFormat=opencover

.PHONY: run
run:
Expand Down
160 changes: 134 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,134 @@

[![codecov](https://codecov.io/gh/jmg1138/trappykeepy/branch/main/graph/badge.svg?token=ARrGqDcKhD)](https://codecov.io/gh/jmg1138/trappykeepy)

A Simple Document Storage [Web API](https://dotnet.microsoft.com/apps/aspnet/apis)
A Simple Document Storage Web API

Users, user roles, user groups, and documents (aka keepers).
## Features

- Users
- Groups
- Document storage
- Document access control

Users may be created and may sign in using their credentials to receive a session token. Groups may be created, and users may be given memberships to groups. Downloading documents requires permission per-document, and document access permits may be issued directly to users, or to groups. Users may be given roles of basic, manager, or admin. Users with the role of manager or admin may upload documents. Administrators may access all API endpoints so that they may manage users, groups, and documents, including user/group memberships/permits.

## Endpoints

Endpoints are organized into CRUD operations by HTTP methods of `GET`, `POST`, `PUT`, and `DELETE`. Resources are named with plural nouns (e.g. `groups`).

Relationships are represented through nested endpoints. To `GET` all memberships of a specific group `id`, the format is `GET /v1/groups/{id}/memberships`.

Endpoints that retrieve many records will return simple objects. So, `GET /v1/users` will return an array of user objects with basic information for each user record, but no nested objects. Endpoints that retrieve a specific record may return complex objects. So, `GET /v1/users/{id}` will return a single user object including nested objects that may contain arrays of relational data such as the user's posted documents, group memberships, and document access permits.

You can [review the Swagger/OpenApi style documentation on SwaggerHub](https://app.swaggerhub.com/apis/nothingworksright/trappykeepy/v0.1.0).

Below is a simplified list of the available endpoints.

### Groups

```
POST /v1/groups
GET /v1/groups
POST /v1/groups/{id}/permits
GET /v1/groups/{id}/permits
DELETE /v1/groups/{id}/permits
GET /v1/groups/{id}
PUT /v1/groups/{id}
DELETE /v1/groups/{id}
GET /v1/groups/{id}/memberships
DELETE /v1/groups/{id}/memberships
DELETE /v1/groups/{gid}/permits/{pid}
```

### Keepers

```
POST /v1/keepers
GET /v1/keepers
GET /v1/keepers/{id}
PUT /v1/keepers/{id}
DELETE /v1/keepers/{id}
GET /v1/keepers/{id}/permits
```

### Sessions

```
POST /v1/sessions
```

### Users

```
POST /v1/users
GET /v1/users
POST /v1/users/{id}/memberships
GET /v1/users/{id}/memberships
DELETE /v1/users/{id}/memberships
POST /v1/users/{id}/permits
GET /v1/users/{id}/permits
DELETE /v1/users/{id}/permits
GET /v1/users/{id}
PUT /v1/users/{id}
DELETE /v1/users/{id}
PUT /v1/users/{id}/password
DELETE /v1/users/{uid}/memberships/{mid}
DELETE /v1/users/{uid}/permits/{pid}
```

### Requests

Review the [controller classes](https://github.com/jmg1138/TrappyKeepy/tree/main/TrappyKeepy.Api/Controllers) to find example requests in the comment blocks for every controller endpoint/method.

Here is one example, sending a `POST` request to `/v1/groups`:

```bash
curl --location --request POST 'https://api.trappykeepy.com/v1/groups' \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "foo",
"description": "bar"
}'
```

### Responses

Standard Http response status codes are used in responses, such as 200 OK, 400 Bad Reqeust, 401 Unauthorized, and 500 Internal Server Error. When responding with a status 400 there will be helpful information included so that the client may make corrections and try again.

Responses are formatted as [JSend](https://github.com/omniti-labs/jsend). The JSON response will include a `status` key that will hold a value of success, fail, or error. Responses may also include key/values of `data` (the requested data), `message` (user-readable message), or `code` (a application code corresponding to the error, distinct from the Http status code).

## Production

After deploying the API into a production environment, there are some things that will need to be setup.

### Env vars

The following development environment variables with development values provide an example of the environment variables required in production. Environment variable values may be set in the `/etc/environment` file on a Linux host system:

```bash
export TKDB_URL="jdbc:postgresql://localhost:15432/keepydb"
export TKDB_USER="dbuser"
export TKDB_OWNER="dbowner"
export TKDB_PASSWORD="dbpass"
export TKDB_MIGRATIONS="filesystem:./TrappyKeepy.Data/Migrations"
export TKDB_CONN_STRING="Host=localhost;Database=keepydb;Port=15432;Username=dbuser;Password=dbpass"
export TK_CRYPTO_KEY="MqSm0P5dMgFSZhEBKpCv4dVKgDrsgrmT"
```

### First Admin

To create the first administrator user in the system, connect to the database as `dbowner` and insert the user by running the tk.users_create function. Here is an example using development values:

```sql
SELECT * FROM tk.users_create('foo', 'passwordfoo', 'foo@trappykeepy.com', 'admin');
```

## Development

Written using [.NET6](https://dotnet.microsoft.com/download/dotnet/6.0) with the [.NET CLI](https://docs.microsoft.com/en-us/dotnet/core/tools/), [C# 10](https://devblogs.microsoft.com/dotnet/welcome-to-csharp-10/), and [PostgreSQL](https://www.postgresql.org/).

## Using the `Makefile`
### Using the `Makefile`

For common actions like clean, restore, migrate, build, and run, a `Makefile` exists in the source code root directory so these could be easily organized as `Makefile` recipes. You can run any of the following commands from the root directory of the project's source code.

Expand All @@ -22,32 +143,22 @@ For common actions like clean, restore, migrate, build, and run, a `Makefile` ex
`make dbscaffold` | Reverse-engineer the database context and model classes from the PostgreSQL database into .NET, overwriting existing classes with the current database structure, by running the `dotnet ef dbcontext scaffold` command with proper arguments. This command reads the database connection string from the Microsoft Secrets Manager (see the Secrets Manager section below).
`make format` | Format the source code of all projects to match the `.editorconfig` file settings by running the `dotnet format` command.
`make build` | Build the project by running `dotnet build` command for the TrappyKeepy.Api project.
`make test` | Execute the unit tests, but running the `dotnet test` command for the TrappyKeepy.Test project. Generates a test coverage report. When run during the GitHub Action CI workflow the test coverage report is uploaded to Codecov.
`make test` | Execute the unit tests, by running the `dotnet test` command for the TrappyKeepy.Test project. Generates a test coverage report. When run during the GitHub Action CI workflow the test coverage report is uploaded to Codecov.
`make run` | Start the TrappyKeepy application by running the `dotnet run --project TrappyKeepy.Api` command.

## Env vars

The following development environment variables with development values provide an example of the environment variables required in production. Environment variable values can be set in the `/etc/environment` file on a Linux host system:
### Database

```bash
export TKDB_URL="jdbc:postgresql://localhost:15432/keepydb"
export TKDB_USER="dbuser"
export TKDB_OWNER="dbowner"
export TKDB_PASSWORD="dbpass"
export TKDB_MIGRATIONS="filesystem:./TrappyKeepy.Data/Migrations"
export TKDB_CONN_STRING="Host=localhost;Database=keepydb;Port=15432;Username=dbuser;Password=dbpass"
export TK_CRYPTO_KEY="MqSm0P5dMgFSZhEBKpCv4dVKgDrsgrmT"
```
For development, a Vagrant box is setup to create a fresh PostgreSQL database instance that is ready to go. Read about [installing Vagrant](https://www.vagrantup.com/docs/installation) if needed. Once installed, run `vagrant up` from the root directory of this code repository where the `Vagrantfile` is located, which will create and configure the guest machine using the `Vagrant-setup/bootstrap.sh` shell script. Some helpful details for accessing this database are available in `Vagrant-setup/access.md`.

## Database
### Tests

The database is PostgreSQL.
Tests are written using xUnit.

There are a few environment variables that must be set related to the database (see the Env vars section above).
When running `make test` it will run the command `dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover`. This includes the end-to-end tests which require a live database to be present, and generates a code coverage report. Code coverage is uploaded to Codecov.io using their Uploader application.

For development, a Vagrant box is setup to create a fresh PostgreSQL database instance that is ready to go. Read about [installing Vagrant](https://www.vagrantup.com/docs/installation) if needed. Once installed, run `vagrant up` from the root directory of this code repository where the `Vagrantfile` is located, which will create and configure the guest machine using the `Vagrant-setup/bootstrap.sh` shell script. Some helpful details for accessing this database are available in `Vagrant-setup/access.md`.
When tests run during the GitHub Action CI workflow the end-to-end tests are skipped because there is no PostgreSQL database present.

## Secret Manager
### Secret Manager

This only applies to the Database-first reverse-engineering / scaffolding to turn the PostgreSQL table types into .NET model types. This is only done in a development environment after making changes to the database.

Expand All @@ -73,10 +184,7 @@ To view all currently stored development secrets, run the following command:
dotnet user-secrets list --project TrappyKeepy.Api
```

## First Admin
## Thank you

To create the first administrator app user, connect to the database as `dbowner` and insert the user by running the tk.users_create function. Here is an example using development values:
Thanks for taking a look at this project.

```sql
SELECT * FROM tk.users_create('foo', 'passwordfoo', 'foo@trappykeepy.com', 'admin');
```
2 changes: 1 addition & 1 deletion TrappyKeepy.Api/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ charset = utf-8-bom
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
dotnet_sort_system_directives_first = false
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
Expand Down
8 changes: 8 additions & 0 deletions TrappyKeepy.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,11 @@
app.UseHttpsRedirection();
app.MapControllers();
app.Run();

// This is here for the end-to-end tests.
// So that I can use IClassFixture<WebApplicationFactory<Program>>
#pragma warning disable CA1050 // Declare types in namespaces
public partial class Program
{
}
#pragma warning restore CA1050 // Declare types in namespaces
2 changes: 1 addition & 1 deletion TrappyKeepy.Data/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ charset = utf-8-bom
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
dotnet_sort_system_directives_first = false
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
Expand Down
2 changes: 1 addition & 1 deletion TrappyKeepy.Domain/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ charset = utf-8-bom
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
dotnet_sort_system_directives_first = false
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
Expand Down
20 changes: 10 additions & 10 deletions TrappyKeepy.Domain/Models/Service/ServiceResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ public class UserServiceResponse : ServiceResponse<IUserDto>, IUserServiceRespon
public IUserComplexDto? ComplexDto { get; set; }

/// <summary>
/// If no user is provided, instantiate a new one in the Item.
/// If no user is provided, leave things null.
/// </summary>
public UserServiceResponse()
{
Item = new UserDto();

}

/// <summary>
Expand Down Expand Up @@ -97,11 +97,11 @@ public class KeeperServiceResponse : ServiceResponse<IKeeperDto>, IKeeperService
public FileContentResult? FileContentResult { get; set; }

/// <summary>
/// If no keeper is provided, instantiate a new one in the Item.
/// If no keeper is provided, leave things null.
/// </summary>
public KeeperServiceResponse()
{
Item = new KeeperDto();

}

/// <summary>
Expand Down Expand Up @@ -138,11 +138,11 @@ public class GroupServiceResponse : ServiceResponse<IGroupDto>, IGroupServiceRes
public IGroupComplexDto? ComplexDto { get; set; }

/// <summary>
/// If no group is provided, instantiate a new one in the Item.
/// If no group is provided, leave things null.
/// </summary>
public GroupServiceResponse()
{
Item = new GroupDto();

}

/// <summary>
Expand Down Expand Up @@ -177,11 +177,11 @@ public interface IMembershipServiceResponse : IServiceResponse<IMembershipDto>
public class MembershipServiceResponse : ServiceResponse<IMembershipDto>, IMembershipServiceResponse
{
/// <summary>
/// If no membership is provided, instantiate a new one in the Item.
/// If no membership is provided, leave things null.
/// </summary>
public MembershipServiceResponse()
{
Item = new MembershipDto();

}

/// <summary>
Expand Down Expand Up @@ -216,11 +216,11 @@ public interface IPermitServiceResponse : IServiceResponse<IPermitDto>
public class PermitServiceResponse : ServiceResponse<IPermitDto>, IPermitServiceResponse
{
/// <summary>
/// If no permit is provided, instantiate a new one in the Item.
/// If no permit is provided, leave things null.
/// </summary>
public PermitServiceResponse()
{
Item = new PermitDto();

}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion TrappyKeepy.Service/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ charset = utf-8-bom
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
dotnet_sort_system_directives_first = false
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
Expand Down
10 changes: 5 additions & 5 deletions TrappyKeepy.Service/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,11 @@ public async Task<IUserServiceResponse> ReadById(IUserServiceRequest request)
}

// Read the user's permits.
var permits = await _uow.permits.ReadByUserId(user.Id);
if (permits.Count() > 0)
{
user.Permits = permits;
}
var permits = await _uow.permits.ReadByUserId(user.Id);
if (permits.Count() > 0)
{
user.Permits = permits;
}

// Map the repository's domain object to a DTO for the response to the controller.
response.ComplexDto = _mapper.Map<UserComplexDto>(user);
Expand Down
2 changes: 1 addition & 1 deletion TrappyKeepy.Test/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ charset = utf-8-bom
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
dotnet_sort_system_directives_first = false
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
Expand Down
Loading

0 comments on commit 04ea922

Please sign in to comment.