More tests and DELETE account

This commit is contained in:
Lilleman auf Larv 2021-06-24 00:42:54 +02:00
parent 2f5a7918d2
commit ee15057b13
11 changed files with 450 additions and 21 deletions

View File

@ -43,4 +43,3 @@ services:
profiles: ["tests"] profiles: ["tests"]
depends_on: depends_on:
- auth - auth
#command: ls -l

View File

@ -2,6 +2,7 @@ package db
import ( import (
"context" "context"
"errors"
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
@ -48,6 +49,36 @@ func (d Db) AccountCreate(input AccountCreateInput) (CreatedAccount, error) {
}, nil }, nil
} }
func (d Db) AccountDel(accountID string) error {
d.Log.Info("Trying to delete account", "accountID", accountID)
_, renewalTokensErr := d.DbPool.Exec(context.Background(), "DELETE FROM \"renewalTokens\" WHERE \"accountId\" = $1;", accountID)
if renewalTokensErr != nil {
d.Log.Error("Could not remove renewal tokens for account", "err", renewalTokensErr.Error(), "accountID", accountID)
return renewalTokensErr
}
_, fieldsErr := d.DbPool.Exec(context.Background(), "DELETE FROM \"accountsFields\" WHERE \"accountId\" = $1;", accountID)
if fieldsErr != nil {
d.Log.Error("Could not remove account fields", "err", fieldsErr.Error(), "accountID", accountID)
return fieldsErr
}
res, err := d.DbPool.Exec(context.Background(), "DELETE FROM accounts WHERE id = $1", accountID)
if err != nil {
d.Log.Error("Could not remove account", "err", err.Error(), "accountID", accountID)
return err
}
if string(res) == "DELETE 0" {
d.Log.Info("Tried to delete account, but none exists", "accountID", accountID)
err := errors.New("No account found for given accountID")
return err
}
return nil
}
// AccountGet fetches an account from the database // AccountGet fetches an account from the database
func (d Db) AccountGet(accountID string, APIKey string, Name string) (Account, error) { func (d Db) AccountGet(accountID string, APIKey string, Name string) (Account, error) {
d.Log.Debug("Trying to get account", "accountID", accountID, "len(APIKey)", len(APIKey)) d.Log.Debug("Trying to get account", "accountID", accountID, "len(APIKey)", len(APIKey))

View File

@ -54,12 +54,105 @@ var doc = `{
} }
], ],
"responses": { "responses": {
"200": { "201": {
"description": "OK", "description": "Created",
"schema": { "schema": {
"$ref": "#/definitions/db.CreatedAccount" "$ref": "#/definitions/db.CreatedAccount"
} }
}, },
"400": {
"description": "Bad Request",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"409": {
"description": "Conflict",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
}
},
"/account/:id": {
"delete": {
"description": "Requires Authorization-header with role \"admin\".\nExample: Authorization: bearer xxx\nWhere \"xxx\" is a valid JWT token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Delete an account",
"operationId": "account-del",
"parameters": [
{
"type": "string",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"401": { "401": {
"description": "Unauthorized", "description": "Unauthorized",
"schema": { "schema": {

View File

@ -38,12 +38,105 @@
} }
], ],
"responses": { "responses": {
"200": { "201": {
"description": "OK", "description": "Created",
"schema": { "schema": {
"$ref": "#/definitions/db.CreatedAccount" "$ref": "#/definitions/db.CreatedAccount"
} }
}, },
"400": {
"description": "Bad Request",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"409": {
"description": "Conflict",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
}
}
}
},
"/account/:id": {
"delete": {
"description": "Requires Authorization-header with role \"admin\".\nExample: Authorization: bearer xxx\nWhere \"xxx\" is a valid JWT token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Delete an account",
"operationId": "account-del",
"parameters": [
{
"type": "string",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.ResJSONError"
}
}
},
"401": { "401": {
"description": "Unauthorized", "description": "Unauthorized",
"schema": { "schema": {

View File

@ -96,10 +96,75 @@ paths:
produces: produces:
- application/json - application/json
responses: responses:
"200": "201":
description: OK description: Created
schema: schema:
$ref: '#/definitions/db.CreatedAccount' $ref: '#/definitions/db.CreatedAccount'
"400":
description: Bad Request
schema:
items:
$ref: '#/definitions/handlers.ResJSONError'
type: array
"401":
description: Unauthorized
schema:
items:
$ref: '#/definitions/handlers.ResJSONError'
type: array
"403":
description: Forbidden
schema:
items:
$ref: '#/definitions/handlers.ResJSONError'
type: array
"409":
description: Conflict
schema:
items:
$ref: '#/definitions/handlers.ResJSONError'
type: array
"415":
description: Unsupported Media Type
schema:
items:
$ref: '#/definitions/handlers.ResJSONError'
type: array
"500":
description: Internal Server Error
schema:
items:
$ref: '#/definitions/handlers.ResJSONError'
type: array
summary: Create an account
/account/:id:
delete:
consumes:
- application/json
description: |-
Requires Authorization-header with role "admin".
Example: Authorization: bearer xxx
Where "xxx" is a valid JWT token
operationId: account-del
parameters:
- description: Account ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"204":
description: No Content
schema:
type: string
"400":
description: Bad Request
schema:
items:
$ref: '#/definitions/handlers.ResJSONError'
type: array
"401": "401":
description: Unauthorized description: Unauthorized
schema: schema:
@ -124,7 +189,7 @@ paths:
items: items:
$ref: '#/definitions/handlers.ResJSONError' $ref: '#/definitions/handlers.ResJSONError'
type: array type: array
summary: Create an account summary: Delete an account
/account/{id}: /account/{id}:
get: get:
consumes: consumes:

48
src/handlers/delete.go Normal file
View File

@ -0,0 +1,48 @@
package handlers
import (
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// AccountDel godoc
// @Summary Delete an account
// @Description Requires Authorization-header with role "admin".
// @Description Example: Authorization: bearer xxx
// @Description Where "xxx" is a valid JWT token
// @ID account-del
// @Accept json
// @Produce json
// @Param id path string true "Account ID"
// @Success 204 {string} string ""
// @Failure 400 {object} []ResJSONError
// @Failure 401 {object} []ResJSONError
// @Failure 403 {object} []ResJSONError
// @Failure 404 {object} []ResJSONError
// @Failure 415 {object} []ResJSONError
// @Failure 500 {object} []ResJSONError
// @Router /account/:id [delete]
func (h Handlers) AccountDel(c *fiber.Ctx) error {
accountID := c.Params("accountID")
_, uuidErr := uuid.Parse(accountID)
if uuidErr != nil {
return c.Status(400).JSON([]ResJSONError{{Error: "Invalid uuid format"}})
}
authErr := h.RequireAdminRole(c)
if authErr != nil {
return c.Status(403).JSON([]ResJSONError{{Error: authErr.Error()}})
}
err := h.Db.AccountDel(accountID)
if err != nil {
if err.Error() == "No account found for given accountID" {
return c.Status(404).JSON([]ResJSONError{{Error: err.Error()}})
} else {
return c.Status(500).JSON([]ResJSONError{{Error: "Database error when trying to remove account"}})
}
}
return c.Status(204).Send(nil)
}

View File

@ -21,7 +21,6 @@ import (
// @Router /account/{id} [get] // @Router /account/{id} [get]
func (h Handlers) AccountGet(c *fiber.Ctx) error { func (h Handlers) AccountGet(c *fiber.Ctx) error {
accountID := c.Params("accountID") accountID := c.Params("accountID")
// logContext := log.WithFields(log.Fields{"accountID": accountID})
authErr := h.RequireAdminRoleOrAccountID(c, accountID) authErr := h.RequireAdminRoleOrAccountID(c, accountID)
if authErr != nil { if authErr != nil {
@ -30,8 +29,12 @@ func (h Handlers) AccountGet(c *fiber.Ctx) error {
account, accountErr := h.Db.AccountGet(accountID, "", "") account, accountErr := h.Db.AccountGet(accountID, "", "")
if accountErr != nil { if accountErr != nil {
if accountErr.Error() == "no rows in result set" {
return c.Status(404).JSON([]ResJSONError{{Error: "No account found for given accountID"}})
} else {
return c.Status(500).JSON([]ResJSONError{{Error: accountErr.Error()}}) return c.Status(500).JSON([]ResJSONError{{Error: accountErr.Error()}})
} }
}
return c.JSON(account) return c.JSON(account)
} }

View File

@ -73,7 +73,9 @@ func (h Handlers) parseHeaders(c *fiber.Ctx) map[string]string {
lineParts := strings.Split(line, ": ") lineParts := strings.Split(line, ": ")
if len(lineParts) == 1 { if len(lineParts) == 1 {
if len(line) != 0 {
h.Log.Debug("Ignoring header line", "line", line) h.Log.Debug("Ignoring header line", "line", line)
}
} else { } else {
headersMap[lineParts[0]] = lineParts[1] headersMap[lineParts[0]] = lineParts[1]
} }

View File

@ -91,6 +91,7 @@ func main() {
app.Get("/swagger", func(c *fiber.Ctx) error { return c.Redirect("/swagger/index.html") }) app.Get("/swagger", func(c *fiber.Ctx) error { return c.Redirect("/swagger/index.html") })
app.Get("/swagger/*", swagger.Handler) app.Get("/swagger/*", swagger.Handler)
app.Delete("/account/:accountID", handlers.AccountDel)
app.Get("/account/:accountID", handlers.AccountGet) app.Get("/account/:accountID", handlers.AccountGet)
app.Post("/account", handlers.AccountCreate) app.Post("/account", handlers.AccountCreate)
app.Post("/auth/api-key", handlers.AccountAuthAPIKey) app.Post("/auth/api-key", handlers.AccountAuthAPIKey)

View File

@ -9,6 +9,4 @@ test('test-cases/00start.js: Wait for auth API to be ready', async t => {
const backendHealthCheck = await got(process.env.AUTH_URL, { retry: 2000 }); const backendHealthCheck = await got(process.env.AUTH_URL, { retry: 2000 });
t.equal(backendHealthCheck.statusCode, 200, 'Auth API should answer with status code 200'); t.equal(backendHealthCheck.statusCode, 200, 'Auth API should answer with status code 200');
t.end();
}); });

View File

@ -3,9 +3,15 @@ import jwt from 'jsonwebtoken'
import setConfig from '../test-helpers/config.js'; import setConfig from '../test-helpers/config.js';
import test from 'tape'; import test from 'tape';
test('test-cases/01basic.js: Basic stuff', async t => { let adminJWT;
t.comment('Authing with configurated API KEY'); let adminJWTString;
let user;
let userJWT;
let userJWTString;
const userName = 'test-tomte nöff #18';
const password = 'lurpassare7½TUR';
test('test-cases/01basic.js: Authing with configurated API KEY', async t => {
// Wrong API key // Wrong API key
try { try {
await got.post(`${process.env.AUTH_URL}/auth/api-key`, { await got.post(`${process.env.AUTH_URL}/auth/api-key`, {
@ -18,18 +24,20 @@ test('test-cases/01basic.js: Basic stuff', async t => {
t.equal(err.message, 'Response code 403 (Forbidden)', 'Calling /auth/api-key with wrong api-key should result in a 403') t.equal(err.message, 'Response code 403 (Forbidden)', 'Calling /auth/api-key with wrong api-key should result in a 403')
} }
// Successful auth
const authRes = await got.post(`${process.env.AUTH_URL}/auth/api-key`, { const authRes = await got.post(`${process.env.AUTH_URL}/auth/api-key`, {
json: 'hihi', json: 'hihi',
responseType: 'json', responseType: 'json',
}); });
t.notEqual(authRes.body.jwt, undefined, 'The body should include a jwt key'); t.notEqual(authRes.body.jwt, undefined, 'The body should include a jwt key');
t.notEqual(authRes.body.renewalToken, undefined, 'The body should include a renewalToken'); t.notEqual(authRes.body.renewalToken, undefined, 'The body should include a renewalToken');
adminJWTString = authRes.body.jwt;
const adminJWT = jwt.verify(authRes.body.jwt, process.env.JWT_SHARED_SECRET); adminJWT = jwt.verify(adminJWTString, process.env.JWT_SHARED_SECRET);
t.equal(adminJWT.accountName, 'admin', 'The verified account name should be "admin"'); t.equal(adminJWT.accountName, 'admin', 'The verified account name should be "admin"');
});
t.comment('GETting the admin account, with the token we just obtained'); test('test-cases/01basic.js: GETting the admin account, with the token we just obtained', async t => {
try { try {
await got(`${process.env.AUTH_URL}/account/${adminJWT.accountId}`); await got(`${process.env.AUTH_URL}/account/${adminJWT.accountId}`);
t.fail('Calling /account/{id} without proper auth token should give 403'); t.fail('Calling /account/{id} without proper auth token should give 403');
@ -38,11 +46,99 @@ test('test-cases/01basic.js: Basic stuff', async t => {
} }
const accountRes = await got(`${process.env.AUTH_URL}/account/${adminJWT.accountId}`, { const accountRes = await got(`${process.env.AUTH_URL}/account/${adminJWT.accountId}`, {
headers: { 'Authorization': `bearer ${authRes.body.jwt}`}, headers: { 'Authorization': `bearer ${adminJWTString}`},
responseType: 'json', responseType: 'json',
}); });
t.equal(adminJWT.accountId, accountRes.body.id, 'The account ids should match'); t.equal(adminJWT.accountId, accountRes.body.id, 'The account ids should match');
});
t.end();
test('test-cases/01basic.js: Creating a new account', async t => {
const res = await got.post(`${process.env.AUTH_URL}/account`, {
headers: { 'Authorization': `bearer ${adminJWTString}`},
json: {
fields: [
{
name: 'nördområde',
values: ['tåg', 'trädgårdstomtar'],
},
{
name: 'role',
values: ['user'],
}
],
name: userName,
password,
},
responseType: 'json',
});
user = res.body;
t.notEqual(user.id, undefined, 'The new account should have an id');
t.notEqual(user.apiKey, undefined, 'The new account should have an apiKey');
try {
await got.post(`${process.env.AUTH_URL}/account`, {
headers: { 'Authorization': `bearer ${adminJWTString}`},
json: {
fields: [{name: 'role',values: ['user'],}],
name: userName,
password,
},
responseType: 'json',
});
t.fail('Trying to create another account with the same name should fail with a 409');
} catch(err) {
t.equal(err.message, 'Response code 409 (Conflict)', 'Trying to create another account with the same name should fail with a 409');
}
});
test('test-cases/01basic.js: Auth by username and password', async t => {
const authRes = await got.post(`${process.env.AUTH_URL}/auth/password`, {
json: {
name: userName,
password,
},
responseType: 'json',
});
t.notEqual(authRes.body.jwt, undefined, 'The body should include a jwt key');
t.notEqual(authRes.body.renewalToken, undefined, 'The body should include a renewalToken');
userJWTString = authRes.body.jwt;
userJWT = jwt.verify(userJWTString, process.env.JWT_SHARED_SECRET);
t.equal(userJWT.accountName, userName, 'The verified account name should match the created user');
});
test('test-cases/01basic.js: Remove an account', async t => {
try {
// Random uuid that should not exist in the db. The chance of this existing is... small
await got.delete(`${process.env.AUTH_URL}/account/a423e690-74b9-4f37-9976-f5bf75a5ea32`, {
headers: { 'Authorization': `bearer ${adminJWTString}`},
responseType: 'json',
retry: 0,
});
t.fail('Response status for DELETing an account that does not exist should be 404');
} catch (err) {
t.equal(err.message, 'Response code 404 (Not Found)', 'Response status for DELETing an account that does not exist should be 404');
}
const delRes = await got.delete(`${process.env.AUTH_URL}/account/${user.id}`, {
headers: { 'Authorization': `bearer ${adminJWTString}`},
responseType: 'json',
retry: 0,
});
t.equal(delRes.statusCode, 204, 'Response status for DELETE should be 204');
try {
await got(`${process.env.AUTH_URL}/account/${user.id}`, {
headers: { 'Authorization': `bearer ${adminJWTString}`},
responseType: 'json',
retry: 0,
});
t.fail('Response status for GETing the account again should be 404');
} catch (err) {
t.equal(err.message, 'Response code 404 (Not Found)', 'Response status for GETing the account again should be 404');
}
}); });