Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fa30b3a32 | |||
| 80ed6033d1 | |||
| 4640522d5d | |||
| 848232cc11 | |||
| 98dedc4494 | |||
| e6f64ea026 | |||
| ea71d3825e | |||
| afa9b33568 | |||
| a93189411b | |||
| 98f103f5cf | |||
| 6fc48dae83 | |||
| d039add340 | |||
| 0214d6f46d | |||
| 37e14ec2f8 | |||
| a85d5a8c73 | |||
| 80cebe3357 | |||
| 451a5fbf0f | |||
| aeb6d1164b | |||
| 2f14cd82dd | |||
| faedd0bc7a | |||
| 1d207fae64 | |||
| a71f345f66 | |||
| 7cc6c1197f | |||
| 115bbf7366 |
@@ -1,6 +1,5 @@
|
||||
.git
|
||||
/bin/
|
||||
/.shards/
|
||||
/bruno/
|
||||
/spec/
|
||||
/sqlite/
|
||||
|
||||
@@ -35,20 +35,26 @@ jobs:
|
||||
- name: Extract version from shard.yml
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION=$(grep -oP 'version:\s*\K\S+' shard.yml)
|
||||
VERSION=$(echo $VERSION | tr -d '\n\r')
|
||||
VERSION=$(grep '^version:' shard.yml | cut -d ' ' -f 2)
|
||||
echo "RELEASE_TAG=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Set tags
|
||||
id: set_tags
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
echo "TAGS=latest,${{ env.RELEASE_TAG }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "TAGS=latest" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
id: push
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
sjdonado/bit:latest
|
||||
${{ github.event_name == 'release' && env.RELEASE_TAG && 'sjdonado/bit:${{ env.RELEASE_TAG }}' || '' }}
|
||||
tags: sjdonado/bit:${{ env.TAGS }}
|
||||
|
||||
- name: Attest
|
||||
uses: actions/attest-build-provenance@v1
|
||||
|
||||
+19
-9
@@ -1,5 +1,6 @@
|
||||
FROM alpine:edge as base
|
||||
FROM alpine:edge AS build
|
||||
|
||||
ENV ENV=production
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk update && apk add --no-cache \
|
||||
@@ -9,20 +10,29 @@ RUN apk update && apk add --no-cache \
|
||||
sqlite-dev \
|
||||
openssl-dev
|
||||
|
||||
FROM base AS build
|
||||
ENV ENV=production
|
||||
|
||||
COPY . .
|
||||
COPY . .
|
||||
|
||||
RUN shards install
|
||||
RUN shards build --progress
|
||||
RUN shards build --release --no-debug
|
||||
|
||||
FROM alpine:edge AS runtime
|
||||
|
||||
ENV ENV=production
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --no-cache \
|
||||
gc \
|
||||
pcre2 \
|
||||
libevent \
|
||||
yaml \
|
||||
sqlite-libs \
|
||||
openssl
|
||||
|
||||
RUN mkdir -p sqlite
|
||||
|
||||
FROM base AS release
|
||||
RUN mkdir -p /usr/src/app/sqlite
|
||||
COPY --from=build /usr/src/app/db db
|
||||
COPY --from=build /usr/src/app/data data
|
||||
COPY --from=build /usr/src/app/bin /usr/local/bin
|
||||
COPY --from=build /usr/src/app/data /usr/local/data
|
||||
|
||||
EXPOSE 4000/tcp
|
||||
CMD ["bit"]
|
||||
|
||||
@@ -1,103 +1,14 @@
|
||||
[](https://hub.docker.com/repository/docker/sjdonado/bit/general)
|
||||
[](https://hub.docker.com/repository/docker/sjdonado/bit/general)
|
||||
[](https://hub.docker.com/repository/docker/sjdonado/bit/general)
|
||||
|
||||
# Benchmark
|
||||
|
||||
```shell
|
||||
$ ./benchmark.sh
|
||||
Semaphore initialized with 2666 slots.
|
||||
Setup...
|
||||
[+] Running 2/2
|
||||
✔ Network bit_default Created 0.0s
|
||||
✔ Container bit-app-1 Started 0.2s
|
||||
2024-07-12T18:41:20.962052Z INFO - micrate: Migrating db, current version: 0, target: 20240711224103
|
||||
2024-07-12T18:41:20.965729Z INFO - micrate: OK 20240512214223_create_links.sql
|
||||
2024-07-12T18:41:20.969198Z INFO - micrate: OK 20240512225208_add_slug_index_to_links.sql
|
||||
2024-07-12T18:41:20.973136Z INFO - micrate: OK 20240513115731_create_users.sql
|
||||
2024-07-12T18:41:20.975525Z INFO - micrate: OK 20240513130054_add_api_key_index_to_users.sql
|
||||
2024-07-12T18:41:20.979195Z INFO - micrate: OK 20240711224103_create_clicks.sql
|
||||
Captured API Key: Z01Qk4M5E0xhggZUCdQAPw
|
||||
Waiting for database to be ready...
|
||||
Creating 1000 short links...
|
||||
Created short link 100/1000
|
||||
Created short link 200/1000
|
||||
Created short link 300/1000
|
||||
Created short link 400/1000
|
||||
Created short link 500/1000
|
||||
Created short link 600/1000
|
||||
Created short link 700/1000
|
||||
Created short link 800/1000
|
||||
Created short link 900/1000
|
||||
Created short link 1000/1000
|
||||
Accessing each link 10 times concurrently...
|
||||
****Results****
|
||||
Average Memory Usage: 16.36 MiB
|
||||
Average CPU Usage: 0%
|
||||
Average Response Time: 12.37 µs
|
||||
```
|
||||
|
||||
# Self-hosted
|
||||
|
||||
- Run via docker-compose
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
|
||||
docker-compose exec -it app migrate
|
||||
docker-compose exec -it app cli --create-user=Admin
|
||||
```
|
||||
|
||||
- Run via docker cli
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--name bit \
|
||||
-p 4000:4000 \
|
||||
-e ENV="production" \
|
||||
-e DATABASE_URL="sqlite3://./sqlite/data.db?journal_mode=wal&synchronous=normal&foreign_keys=true" \
|
||||
-e APP_URL="http://localhost:4000" \
|
||||
sjdonado/bit
|
||||
|
||||
docker exec -it bit migrate
|
||||
docker exec -it bit cli --create-user=Admin
|
||||
```
|
||||
|
||||
- Dokku
|
||||
|
||||
```dockerfile
|
||||
FROM sjdonado/bit
|
||||
```
|
||||
|
||||
```bash
|
||||
dokku apps:create bit
|
||||
|
||||
dokku domains:set bit bit.donado.co
|
||||
dokku letsencrypt:enable bit
|
||||
|
||||
dokku storage:ensure-directory bit-sqlite
|
||||
dokku storage:mount bit /var/lib/dokku/data/storage/bit-sqlite:/usr/src/app/sqlite/
|
||||
|
||||
dokku config:set bit DATABASE_URL="sqlite3://./sqlite/data.db?journal_mode=wal&synchronous=normal&foreign_keys=true" APP_URL=https://bit.donado.co
|
||||
|
||||
dokku ports:add bit http:80:4000
|
||||
dokku ports:add bit https:443:4000
|
||||
|
||||
dokku run bit migrate
|
||||
dokku run bit cli --create-user=Admin
|
||||
```
|
||||
|
||||
# Usage
|
||||
[](https://hub.docker.com/repository/docker/sjdonado/bit)
|
||||
[](https://hub.docker.com/repository/docker/sjdonado/bit)
|
||||
[](https://hub.docker.com/repository/docker/sjdonado/bit)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
1. **Ping the API**
|
||||
|
||||
- **Endpoint**: `/api/ping`
|
||||
- **HTTP Method**: GET
|
||||
- **Description**: Ping the API to check if it's running
|
||||
- **Payload**: -
|
||||
- **Response Example**:
|
||||
- Endpoint: `GET /api/ping`
|
||||
- Payload: None
|
||||
- Response Example
|
||||
```json
|
||||
{
|
||||
"message": "pong"
|
||||
@@ -106,12 +17,10 @@ dokku run bit cli --create-user=Admin
|
||||
|
||||
2. **Retrieve a link by its slug**
|
||||
|
||||
- **Endpoint**: `/:slug`
|
||||
- **HTTP Method**: GET
|
||||
- **Description**: Retrieve a link by its slug
|
||||
- **Payload**: -
|
||||
- **Headers**: `X-Api-Key`
|
||||
- **Response Example**:
|
||||
- Endpoint: `GET /:slug`
|
||||
- Headers: `X-Api-Key`
|
||||
- Payload: None
|
||||
- Response Example
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
@@ -135,12 +44,10 @@ dokku run bit cli --create-user=Admin
|
||||
|
||||
3. **Retrieve all links**
|
||||
|
||||
- **Endpoint**: `/api/links`
|
||||
- **HTTP Method**: GET
|
||||
- **Description**: Retrieve all links
|
||||
- **Payload**: -
|
||||
- **Headers**: `X-Api-Key`
|
||||
- **Response Example**:
|
||||
- Endpoint: `GET /api/links`
|
||||
- Headers: `X-Api-Key`
|
||||
- Payload: None
|
||||
- Response Example
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
@@ -166,12 +73,10 @@ dokku run bit cli --create-user=Admin
|
||||
|
||||
4. **Retrieve a link by its ID**
|
||||
|
||||
- **Endpoint**: `/api/links/:id`
|
||||
- **HTTP Method**: GET
|
||||
- **Description**: Retrieve a link by its ID
|
||||
- **Payload**: -
|
||||
- **Headers**: `X-Api-Key`
|
||||
- **Response Example**:
|
||||
- Endpoint: `GET /api/links/:id`
|
||||
- Headers: `X-Api-Key`
|
||||
- Payload: None
|
||||
- Response Example
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
@@ -195,17 +100,15 @@ dokku run bit cli --create-user=Admin
|
||||
|
||||
5. **Create a new link**
|
||||
|
||||
- **Endpoint**: `/api/links`
|
||||
- **HTTP Method**: POST
|
||||
- **Description**: Create a new link
|
||||
- **Payload**:
|
||||
- Endpoint\*\*: `POST /api/links`
|
||||
- Payload:
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com"
|
||||
}
|
||||
```
|
||||
- **Headers**: `X-Api-Key`
|
||||
- **Response Example**:
|
||||
- Headers: `X-Api-Key`
|
||||
- Response Example:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
@@ -219,17 +122,15 @@ dokku run bit cli --create-user=Admin
|
||||
|
||||
6. **Update an existing link by its ID**
|
||||
|
||||
- **Endpoint**: `/api/links/:id`
|
||||
- **HTTP Method**: PUT
|
||||
- **Description**: Update an existing link by its ID
|
||||
- **Payload**:
|
||||
- Endpoint: `PUT /api/links/:id`
|
||||
- Payload:
|
||||
```json
|
||||
{
|
||||
"url": "https://newexample.com"
|
||||
}
|
||||
```
|
||||
- **Headers**: `X-Api-Key`
|
||||
- **Response Example**:
|
||||
- Headers: `X-Api-Key`
|
||||
- Response Example:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
@@ -243,12 +144,10 @@ dokku run bit cli --create-user=Admin
|
||||
|
||||
7. **Delete a link by its ID**
|
||||
|
||||
- **Endpoint**: `/api/links/:id`
|
||||
- **HTTP Method**: DELETE
|
||||
- **Description**: Delete a link by its ID
|
||||
- **Payload**: -
|
||||
- **Headers**: `X-Api-Key`
|
||||
- **Response Example**:
|
||||
- Endpoint: `DELETE /api/links/:id`
|
||||
- Payload: None
|
||||
- Headers: `X-Api-Key`
|
||||
- Response Example:
|
||||
```json
|
||||
{
|
||||
"message": "Link deleted"
|
||||
@@ -265,33 +164,126 @@ Options:
|
||||
--delete-user=USER_ID Delete a user by ID
|
||||
```
|
||||
|
||||
# Development
|
||||
## Benchmark
|
||||
|
||||
1. **Installation**
|
||||
```
|
||||
$ ./benchmark.sh
|
||||
Setting up...
|
||||
[+] Running 3/3
|
||||
✔ Network bit_default Created 0.0s
|
||||
✔ Volume "bit_sqlite_data" Created 0.0s
|
||||
✔ Container bit Started 0.1s
|
||||
Captured API Key: aHOCnZSuo2kOHy2mDa-iOA
|
||||
Waiting for the application to be ready...
|
||||
HTTP/1.1 200 OK
|
||||
Connection: keep-alive
|
||||
Content-Type: application/json
|
||||
Date: Sun, 27 Oct 2024 11:52:33 GMT
|
||||
Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
|
||||
Access-Control-Allow-Headers: Content-Type, Accept, Origin, X-Api-Key
|
||||
Content-Length: 13
|
||||
|
||||
Starting resource usage monitoring...
|
||||
Creating 10000 short links with 100 conrurrent requests...
|
||||
Link creation complete: 10000 links created.
|
||||
Fetching all created links from /api/links...
|
||||
Selected link for benchmarking: http://localhost:4000/UaVZjA
|
||||
Starting benchmark with Bombardier...
|
||||
Bombarding http://localhost:4000/oEKLAg with 10000 request(s) using 100 connection(s)
|
||||
10000 / 10000 [===============================================================================] 100.00% 830/s 12s
|
||||
Done!
|
||||
Statistics Avg Stdev Max
|
||||
Reqs/sec 853.89 1625.49 8942.54
|
||||
Latency 118.48ms 11.52ms 142.58ms
|
||||
HTTP codes:
|
||||
1xx - 0, 2xx - 0, 3xx - 10000, 4xx - 0, 5xx - 0
|
||||
others - 0
|
||||
Throughput: 360.02KB/s
|
||||
Benchmark completed.
|
||||
Analyzing resource usage...
|
||||
**** Results ****
|
||||
Average CPU Usage: 40.68%
|
||||
Average Memory Usage: 28.62 MiB
|
||||
./benchmark.sh: line 135: 61567 Terminated: 15 monitor_resource_usage
|
||||
[+] Running 2/2
|
||||
✔ Container bit Removed 10.1s
|
||||
✔ Network bit_default Removed
|
||||
```
|
||||
|
||||
## Self-hosted
|
||||
|
||||
### Run via docker-compose
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
|
||||
# Generate an api key
|
||||
docker-compose exec -it app cli --create-user=Admin
|
||||
```
|
||||
|
||||
### Run via docker cli
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--name bit \
|
||||
-p 4000:4000 \
|
||||
-e ENV="production" \
|
||||
-e DATABASE_URL="sqlite3://./sqlite/data.db?journal_mode=wal&synchronous=normal&foreign_keys=true" \
|
||||
-e APP_URL="http://localhost:4000" \
|
||||
sjdonado/bit
|
||||
|
||||
docker exec -it bit cli --create-user=Admin
|
||||
```
|
||||
|
||||
### Dokku
|
||||
|
||||
```dockerfile
|
||||
FROM sjdonado/bit
|
||||
```
|
||||
|
||||
```bash
|
||||
dokku apps:create bit
|
||||
|
||||
dokku domains:set bit bit.donado.co
|
||||
dokku letsencrypt:enable bit
|
||||
|
||||
dokku storage:ensure-directory bit-sqlite
|
||||
dokku storage:mount bit /var/lib/dokku/data/storage/bit-sqlite:/usr/src/app/sqlite/
|
||||
|
||||
dokku config:set bit DATABASE_URL="sqlite3://./sqlite/data.db?journal_mode=wal&synchronous=normal&foreign_keys=true" APP_URL=https://bit.donado.co
|
||||
|
||||
dokku ports:add bit http:80:4000
|
||||
dokku ports:add bit https:443:4000
|
||||
|
||||
dokku run bit cli --create-user=Admin
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
- Setup
|
||||
```bash
|
||||
brew tap amberframework/micrate
|
||||
brew install micrate
|
||||
```
|
||||
|
||||
```bash
|
||||
shards run migrate
|
||||
shards run bit
|
||||
```
|
||||
|
||||
2. **Generate the `X-Api-Key`**
|
||||
- Generate the `X-Api-Key`
|
||||
|
||||
```bash
|
||||
shards run cli -- --create-user=Admin
|
||||
```
|
||||
|
||||
# Run tests
|
||||
- Run tests
|
||||
|
||||
```bash
|
||||
ENV=test crystal spec
|
||||
```
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
1. Fork it (<https://github.com/sjdonado/bit/fork>)
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
require "log"
|
||||
|
||||
ENV["ENV"] ||= "development"
|
||||
ENV["APP_URL"] ||= "http://localhost:4000"
|
||||
ENV["DATABASE_URL"] ||= "sqlite3://./sqlite/data.db?journal_mode=wal&synchronous=normal&foreign_keys=true
|
||||
"
|
||||
|
||||
{% if env("ENV") != "production" %}
|
||||
require "dotenv"
|
||||
Dotenv.load ".env.#{ENV["ENV"]}" # File must exist in non-production!
|
||||
{% end %}
|
||||
|
||||
{% if env("ENV") == "production" %}
|
||||
Log.setup(:error)
|
||||
{% end %}
|
||||
|
||||
@@ -3,3 +3,5 @@ require "kemal"
|
||||
Kemal.config.env = ENV["ENV"]? || "development"
|
||||
Kemal.config.port = ENV["PORT"]?.try(&.to_i) || 4000
|
||||
Kemal.config.host_binding = ENV["HOST"]? || "0.0.0.0"
|
||||
|
||||
Kemal.config.powered_by_header = false
|
||||
|
||||
+14
-9
@@ -9,6 +9,7 @@ module App::Controllers::Link
|
||||
class Create < App::Lib::BaseController
|
||||
include App::Models
|
||||
include App::Lib
|
||||
include App::Services
|
||||
|
||||
def call(env)
|
||||
user = env.get("user").as(User)
|
||||
@@ -26,14 +27,7 @@ module App::Controllers::Link
|
||||
link.id = UUID.v4.to_s
|
||||
link.url = url
|
||||
link.user = user
|
||||
|
||||
loop do
|
||||
slug = Random::Secure.urlsafe_base64(4).gsub(/[^a-zA-Z0-9]/, "")
|
||||
if !Database.get_by(Link, slug: slug)
|
||||
link.slug = slug
|
||||
break
|
||||
end
|
||||
end
|
||||
link.slug = SlugService.shorten_url(url, user.id.to_s)
|
||||
|
||||
changeset = Database.insert(link)
|
||||
if !changeset.valid?
|
||||
@@ -124,6 +118,7 @@ module App::Controllers::Link
|
||||
class Update < App::Lib::BaseController
|
||||
include App::Models
|
||||
include App::Lib
|
||||
include App::Services
|
||||
|
||||
def call(env)
|
||||
user = env.get("user").as(User)
|
||||
@@ -136,7 +131,17 @@ module App::Controllers::Link
|
||||
raise App::NotFoundException.new(env) if link.nil?
|
||||
raise App::ForbiddenException.new(env) if link.user_id != user.id
|
||||
|
||||
link.url = body["url"].to_s
|
||||
new_url = body["url"].to_s
|
||||
|
||||
existing_query = Database::Query.where(url: new_url, user_id: user.id.to_s).limit(1)
|
||||
existing_link = Database.all(Link, existing_query).first?
|
||||
|
||||
if existing_link
|
||||
raise App::UnprocessableEntityException.new(env, { "url" => ["URL already exists"] })
|
||||
end
|
||||
|
||||
link.url = new_url
|
||||
link.slug = SlugService.shorten_url(new_url, user.id.to_s)
|
||||
|
||||
changeset = Database.update(link)
|
||||
if !changeset.valid?
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require "sqlite3"
|
||||
require "crecto"
|
||||
require"micrate"
|
||||
|
||||
module App::Lib
|
||||
class Database
|
||||
@@ -14,5 +15,12 @@ module App::Lib
|
||||
if ENV["ENV"] == "development"
|
||||
Crecto::DbLogger.set_handler(STDOUT)
|
||||
end
|
||||
|
||||
def self.run_migrations
|
||||
Micrate::DB.connection_url = ENV["DATABASE_URL"]
|
||||
Micrate::Cli.run_up
|
||||
end
|
||||
|
||||
run_migrations
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
require "kemal"
|
||||
|
||||
module App
|
||||
class InternalServerErrorException < Kemal::Exceptions::CustomException
|
||||
def initialize(context)
|
||||
context.response.content_type = "application/json"
|
||||
context.response.status_code = 500
|
||||
context.response.print({ "error" => "Internal Server Error" }.to_json)
|
||||
super(context)
|
||||
end
|
||||
end
|
||||
|
||||
class BadRequestException < Kemal::Exceptions::CustomException
|
||||
def initialize(context, message : String)
|
||||
context.response.content_type = "application/json"
|
||||
context.response.status_code = 400
|
||||
context.response.print({ "error" => message }.to_json)
|
||||
super(context)
|
||||
@@ -11,13 +21,16 @@ module App
|
||||
|
||||
class UnauthorizedException < Kemal::Exceptions::CustomException
|
||||
def initialize(context)
|
||||
context.response.content_type = "application/json"
|
||||
context.response.status_code = 401
|
||||
context.response.print({ "error" => "Unauthorized access" }.to_json)
|
||||
super(context)
|
||||
end
|
||||
end
|
||||
|
||||
class ForbiddenException < Kemal::Exceptions::CustomException
|
||||
def initialize(context)
|
||||
context.response.content_type = "application/json"
|
||||
context.response.status_code = 403
|
||||
context.response.print({ "error" => "Access not allowed" }.to_json)
|
||||
super(context)
|
||||
@@ -26,16 +39,28 @@ module App
|
||||
|
||||
class NotFoundException < Kemal::Exceptions::CustomException
|
||||
def initialize(context)
|
||||
context.response.content_type = "application/json"
|
||||
context.response.status_code = 404
|
||||
context.response.print({ "error" => "Resource not found" }.to_json)
|
||||
super(context)
|
||||
end
|
||||
end
|
||||
|
||||
class UnprocessableEntityException < Kemal::Exceptions::CustomException
|
||||
def initialize(context, message : Hash(String, Array(String)))
|
||||
context.response.content_type = "application/json"
|
||||
context.response.status_code = 422
|
||||
context.response.print({ "errors" => message }.to_json)
|
||||
super(context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
error 500 do |env|
|
||||
App::InternalServerErrorException.new(env)
|
||||
""
|
||||
end
|
||||
|
||||
error 404 do |env|
|
||||
""
|
||||
end
|
||||
|
||||
+1
-1
@@ -17,6 +17,6 @@ module App::Models
|
||||
unique_constraint :slug
|
||||
|
||||
validate_required [:slug, :url]
|
||||
validate_format :url, /\A(?:https?:\/\/)?(?:[\w-]+\.)+[\w-]+(?:\/\S*)?/
|
||||
validate_format :url, /\A(?:(https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,})(?::\d+)?(?:[\/?#]\S*)?\z/i
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
require "digest"
|
||||
require "base64"
|
||||
|
||||
module App::Services::SlugService
|
||||
def self.shorten_url(url : String, user_id : String) : String
|
||||
combined = "#{user_id}-#{url}"
|
||||
crc32_hash = Digest::CRC32.digest(combined)
|
||||
base62_encoded = Base64.urlsafe_encode(crc32_hash).strip.tr("-_=", "")
|
||||
|
||||
base62_encoded
|
||||
end
|
||||
end
|
||||
+128
-126
@@ -1,154 +1,156 @@
|
||||
#!/bin/bash
|
||||
|
||||
api_url="http://localhost:4001/api/links"
|
||||
num_links=1000
|
||||
num_requests=10
|
||||
resource_usage_interval=1 # Interval in seconds for resource usage logging
|
||||
# Configuration variables
|
||||
server_url="http://localhost:4000"
|
||||
api_url="${server_url}/api/links"
|
||||
num_links=10000
|
||||
num_requests=10000
|
||||
concurrency=100
|
||||
resource_usage_interval=1
|
||||
container_name="bit"
|
||||
|
||||
semaphore="/tmp/semaphore"
|
||||
max_concurrent_processes=$(ulimit -u) # Adjust this number based on your system's capability
|
||||
check_dependencies() {
|
||||
if ! command -v bombardier &> /dev/null; then
|
||||
echo "Error: bombardier is not installed. Please install it to proceed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Initialize semaphore
|
||||
mkfifo $semaphore
|
||||
exec 3<> $semaphore
|
||||
rm $semaphore
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is not installed. Please install it to proceed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
for ((i=0; i<max_concurrent_processes; i++)); do
|
||||
echo >&3
|
||||
done
|
||||
setup_containers() {
|
||||
echo "Setting up..."
|
||||
docker compose up -d
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to start Docker containers."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Semaphore initialized with $max_concurrent_processes slots."
|
||||
output=$(docker compose exec -T app cli --create-user=Admin)
|
||||
api_key=$(echo "$output" | awk -F' ' '/X-Api-Key:/{print $NF}')
|
||||
echo "Captured API Key: $api_key"
|
||||
|
||||
function get_resource_usage {
|
||||
while true; do
|
||||
docker stats --no-stream --format "table {{.MemUsage}} {{.CPUPerc}}" bit-app-1 | awk 'NR>1 {print "Memory:", $1, "CPU:", $2}' >> resource_usage.txt
|
||||
if [[ -z "$api_key" ]]; then
|
||||
echo "Error: API key could not be retrieved."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for the application to be ready..."
|
||||
until curl --silent --head --fail --header "X-Api-Key: $api_key" "$server_url/api/ping"; do
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
monitor_resource_usage() {
|
||||
echo "Starting resource usage monitoring..."
|
||||
echo "Timestamp,CPU,Memory" > resource_usage.csv
|
||||
while :; do
|
||||
stats=$(docker stats --no-stream --format "{{.CPUPerc}},{{.MemUsage}}" $container_name)
|
||||
cpu=$(echo $stats | awk -F',' '{print $1}' | sed 's/%//')
|
||||
mem=$(echo $stats | awk -F',' '{print $2}' | awk '{print $1}')
|
||||
timestamp=$(date +%s)
|
||||
echo "$timestamp,$cpu,$mem" >> resource_usage.csv
|
||||
sleep $resource_usage_interval
|
||||
done
|
||||
}
|
||||
|
||||
function calculate_average_usage {
|
||||
total_mem=0
|
||||
total_cpu=0
|
||||
count=0
|
||||
create_links() {
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
while read -r line; do
|
||||
if echo $line | grep -q 'Memory'; then
|
||||
mem=$(echo $line | awk '{print $2}' | sed 's/MiB//')
|
||||
total_mem=$(echo "$total_mem + $mem" | bc)
|
||||
elif echo $line | grep -q 'CPU'; then
|
||||
cpu=$(echo $line | awk '{print $2}' | sed 's/%//')
|
||||
total_cpu=$(echo "$total_cpu + $cpu" | bc)
|
||||
fi
|
||||
((count++))
|
||||
done < resource_usage.txt
|
||||
echo "Creating $num_links short links with $concurrency conrurrent requests..."
|
||||
|
||||
avg_mem=$(echo "scale=2; $total_mem / ($count / 2)" | bc) # Since there are 2 lines per interval
|
||||
avg_cpu=$(echo "scale=2; $total_cpu / ($count / 2)" | bc)
|
||||
rm resource_usage.txt
|
||||
# Populate URLs into a file to feed into curl
|
||||
for ((i=1; i<=num_links; i++)); do
|
||||
url="https://example.com/${i}-${num_links}"
|
||||
echo "--next" >> "$temp_file"
|
||||
echo "--request POST" >> "$temp_file"
|
||||
echo "--url \"$api_url\"" >> "$temp_file"
|
||||
echo "--header \"X-Api-Key: $api_key\"" >> "$temp_file"
|
||||
echo "--header \"Content-Type: application/json\"" >> "$temp_file"
|
||||
echo "--data \"{ \\\"url\\\": \\\"$url\\\" }\"" >> "$temp_file"
|
||||
done
|
||||
|
||||
echo "Average Memory Usage: $avg_mem MiB"
|
||||
echo "Average CPU Usage: $avg_cpu%"
|
||||
curl --parallel --parallel-immediate --parallel-max $concurrency --config "$temp_file" --silent --write-out "%{http_code}\n" > /dev/null
|
||||
|
||||
echo "Link creation complete: $num_links links created."
|
||||
|
||||
# Clean up
|
||||
rm -f "$temp_file"
|
||||
}
|
||||
|
||||
function measure {
|
||||
total_time=0
|
||||
declare -a refer_links
|
||||
run_benchmark() {
|
||||
echo "Fetching all created links from /api/links..."
|
||||
all_links_response=$(curl --silent --request GET \
|
||||
--url "$api_url" \
|
||||
--header "X-Api-Key: $api_key" \
|
||||
--header "Content-Type: application/json")
|
||||
|
||||
# Start resource usage logging in the background
|
||||
nohup bash -c "$(declare -f get_resource_usage); get_resource_usage" &> /dev/null &
|
||||
resource_usage_pid=$!
|
||||
disown
|
||||
|
||||
echo "Creating $num_links short links..."
|
||||
for ((i=1; i<=num_links; i++)); do
|
||||
response=$(curl --silent --request POST \
|
||||
--url $api_url \
|
||||
--header "X-Api-Key: $api_key" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "{ \"url\": \"https://kagi.com\" }")
|
||||
refer=$(echo $response | awk -F'"' '/"refer":/{print $(NF-1)}')
|
||||
|
||||
if [[ -n $refer ]]; then
|
||||
refer_links+=("$refer")
|
||||
if (( i % 100 == 0 )); then
|
||||
echo "Created short link $i/$num_links"
|
||||
fi
|
||||
else
|
||||
echo "Failed to create short link $i"
|
||||
echo $response
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Accessing each link $num_requests times concurrently..."
|
||||
> times.txt # Ensure times.txt is created and empty
|
||||
|
||||
total_accesses=$((num_links * num_requests))
|
||||
accesses_done=0
|
||||
|
||||
for refer in "${refer_links[@]}"; do
|
||||
for ((i=1; i<=num_requests; i++)); do
|
||||
# Wait for a slot
|
||||
read -u 3
|
||||
{
|
||||
start_time=$(date +%s%6N)
|
||||
curl -s "$refer" >> /dev/null
|
||||
end_time=$(date +%s%6N)
|
||||
elapsed_time=$(echo "$end_time - $start_time" | bc)
|
||||
echo $elapsed_time >> times.txt
|
||||
# Release the slot
|
||||
echo >&3
|
||||
|
||||
((accesses_done++))
|
||||
if (( accesses_done % 10 == 0 )); then
|
||||
echo "Accessed $accesses_done/$total_accesses"
|
||||
fi
|
||||
} &
|
||||
done
|
||||
done
|
||||
|
||||
wait
|
||||
|
||||
# Stop resource usage logging
|
||||
if kill -0 $resource_usage_pid 2>/dev/null; then
|
||||
kill $resource_usage_pid
|
||||
links=($(echo "$all_links_response" | jq -r '.data[] | .refer'))
|
||||
if [[ ${#links[@]} -ne $num_links ]]; then
|
||||
echo "Error: Expected $num_links links but found ${#links[@]}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read all elapsed times and calculate total
|
||||
while read -r time; do
|
||||
total_time=$(echo "$total_time + $time" | bc)
|
||||
done < times.txt
|
||||
rm times.txt
|
||||
random_link="${links[RANDOM % ${#links[@]}]}"
|
||||
echo "Selected link for benchmarking: $random_link"
|
||||
|
||||
echo "****Results****"
|
||||
|
||||
calculate_average_usage
|
||||
echo "Average Response Time: $(echo "scale=2; $total_time / ($num_links * $num_requests)" | bc) µs"
|
||||
echo "Starting benchmark with Bombardier..."
|
||||
bombardier -c $concurrency -n $num_requests "$random_link"
|
||||
echo "Benchmark completed."
|
||||
}
|
||||
|
||||
echo "Setup..."
|
||||
analyze_resource_usage() {
|
||||
echo "Analyzing resource usage..."
|
||||
total_cpu=0
|
||||
total_mem=0
|
||||
count=0
|
||||
|
||||
docker-compose up -d
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to start Docker containers."
|
||||
exit 1
|
||||
fi
|
||||
while IFS=',' read -r timestamp cpu mem; do
|
||||
# Skip header line and lines with empty cpu or mem values
|
||||
if [[ $timestamp != "Timestamp" && -n $cpu && -n $mem ]]; then
|
||||
mem=${mem%MiB}
|
||||
|
||||
docker-compose exec -T app migrate
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to run database migrations."
|
||||
exit 1
|
||||
fi
|
||||
total_cpu=$(echo "$total_cpu + $cpu" | bc)
|
||||
total_mem=$(echo "$total_mem + $mem" | bc)
|
||||
((count++))
|
||||
fi
|
||||
done < resource_usage.csv
|
||||
|
||||
# Create a new user and capture the API key
|
||||
output=$(docker-compose exec -T app cli --create-user=Admin)
|
||||
api_key=$(echo "$output" | awk -F' ' '/X-Api-Key:/{print $NF}')
|
||||
echo "Captured API Key: $api_key"
|
||||
avg_cpu=0.00
|
||||
avg_mem=0.00
|
||||
|
||||
echo "Waiting for database to be ready..."
|
||||
sleep 5
|
||||
if (( count > 0 )); then
|
||||
avg_cpu=$(echo "scale=2; $total_cpu / $count" | bc)
|
||||
avg_mem=$(echo "scale=2; $total_mem / $count" | bc)
|
||||
fi
|
||||
|
||||
measure
|
||||
echo "**** Results ****"
|
||||
echo "Average CPU Usage: $avg_cpu%"
|
||||
echo "Average Memory Usage: $avg_mem MiB"
|
||||
}
|
||||
|
||||
# Clean up
|
||||
docker-compose down
|
||||
cleanup() {
|
||||
rm -f resource_usage.csv
|
||||
docker compose down
|
||||
}
|
||||
|
||||
main() {
|
||||
check_dependencies
|
||||
setup_containers
|
||||
|
||||
monitor_resource_usage & # Start monitoring in the background
|
||||
monitor_pid=$!
|
||||
trap 'kill $monitor_pid; cleanup; exit' INT
|
||||
|
||||
create_links
|
||||
run_benchmark
|
||||
|
||||
kill $monitor_pid
|
||||
analyze_resource_usage
|
||||
cleanup
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
@@ -5,14 +5,11 @@ require "./app/lib/*"
|
||||
require "./app/models/*"
|
||||
require "./app/serializers/*"
|
||||
require "./app/middlewares/*"
|
||||
require "./app/services/*"
|
||||
|
||||
require "./app/routes"
|
||||
|
||||
add_context_storage_type(App::Models::User)
|
||||
add_handler(App::Middlewares::Auth.new)
|
||||
|
||||
error 500 { |env| {"error" => "Internal Server Error" }.to_json}
|
||||
error 401 { |env| {"error" => "Unauthorized" }.to_json}
|
||||
error 404 { |env| {"error" => "Not Found" }.to_json}
|
||||
|
||||
Kemal.run
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
-- +micrate Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
|
||||
-- Step 1: Create a new table with the desired column type
|
||||
CREATE TABLE links_new (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
slug VARCHAR(8) UNIQUE NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
created_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Step 2: Copy data from the old table to the new table
|
||||
INSERT INTO links_new (id, user_id, slug, url, created_at, updated_at)
|
||||
SELECT id, user_id, slug, url, created_at, updated_at FROM links;
|
||||
|
||||
-- Step 3: Drop the old table
|
||||
DROP TABLE links;
|
||||
|
||||
-- Step 4: Rename the new table to the old table's name
|
||||
ALTER TABLE links_new RENAME TO links;
|
||||
|
||||
-- +micrate Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
||||
-- Step 1: Create a new table with the original column type
|
||||
CREATE TABLE links_old (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
slug VARCHAR(4) UNIQUE NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
created_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Step 2: Copy data from the current table to the old table
|
||||
INSERT INTO links_old (id, user_id, slug, url, created_at, updated_at)
|
||||
SELECT id, user_id, substr(slug, 1, 4), url, created_at, updated_at FROM links;
|
||||
|
||||
-- Step 3: Drop the current table
|
||||
DROP TABLE links;
|
||||
|
||||
-- Step 4: Rename the old table to the current table's name
|
||||
ALTER TABLE links_old RENAME TO links;
|
||||
+7
-3
@@ -1,9 +1,13 @@
|
||||
services:
|
||||
app:
|
||||
container_name: bit
|
||||
build: .
|
||||
environment:
|
||||
ENV: production
|
||||
DATABASE_URL: sqlite3://./sqlite/data.db?journal_mode=wal&synchronous=normal&foreign_keys=true
|
||||
APP_URL: http://0.0.0.0:4001
|
||||
ports:
|
||||
- 4001:4000
|
||||
- 4000:4000
|
||||
volumes:
|
||||
- sqlite_data:/app/sqlite
|
||||
|
||||
volumes:
|
||||
sqlite_data:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
require "sqlite3"
|
||||
require"micrate"
|
||||
|
||||
require "../app/config/env"
|
||||
|
||||
Micrate::DB.connection_url = ENV["DATABASE_URL"]
|
||||
Micrate::Cli.run_up
|
||||
@@ -1,5 +1,5 @@
|
||||
name: bit
|
||||
version: 1.1.0
|
||||
version: 1.2.1
|
||||
|
||||
authors:
|
||||
- Juan Rodriguez <sjdonado@icloud.com>
|
||||
@@ -9,8 +9,6 @@ targets:
|
||||
main: bit.cr
|
||||
cli:
|
||||
main: scripts/cli.cr
|
||||
migrate:
|
||||
main: scripts/migrate.cr
|
||||
|
||||
dependencies:
|
||||
kemal:
|
||||
|
||||
@@ -22,7 +22,7 @@ describe "App::Controllers::Link" do
|
||||
it "should return existing link if url already exists" do
|
||||
test_user = create_test_user()
|
||||
|
||||
payload = {"url" => "https://kagi.com"}
|
||||
payload = {"url" => "http://idonthavespotify.donado.co"}
|
||||
post(
|
||||
"/api/links",
|
||||
headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
|
||||
@@ -75,7 +75,7 @@ describe "App::Controllers::Link" do
|
||||
payload = {"url" => "https://kagi.com"}
|
||||
post("/api/links", headers: HTTP::Headers{"Content-Type" => "application/json"}, body: payload.to_json)
|
||||
|
||||
expected = {"error" => "Unauthorized"}.to_json
|
||||
expected = {"error" => "Unauthorized access"}.to_json
|
||||
response.status_code.should eq(401)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
@@ -83,7 +83,7 @@ describe "App::Controllers::Link" do
|
||||
|
||||
describe "Index" do
|
||||
it "should redirect to origin domain" do
|
||||
link = "https://kagi.com"
|
||||
link = "https://test.com"
|
||||
test_user = create_test_user()
|
||||
|
||||
test_link = create_test_link(test_user, link)
|
||||
@@ -95,7 +95,7 @@ describe "App::Controllers::Link" do
|
||||
end
|
||||
|
||||
it "should create a new click after redirect" do
|
||||
link = "https://kagi.com"
|
||||
link = "https://sjdonado.com"
|
||||
test_user = create_test_user()
|
||||
|
||||
test_link = create_test_link(test_user, link)
|
||||
@@ -112,17 +112,11 @@ describe "App::Controllers::Link" do
|
||||
end
|
||||
|
||||
it "should return 404 - link does not exist" do
|
||||
link = "https://kagi.com"
|
||||
test_user = create_test_user()
|
||||
|
||||
test_link = create_test_link(test_user, link)
|
||||
serialized_link = App::Serializers::Link.new(test_link)
|
||||
get("https://localhost:4001/R4kj2")
|
||||
|
||||
delete_test_link(test_link.id)
|
||||
|
||||
get(serialized_link.refer, headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})
|
||||
|
||||
expected = {"error" => "Not Found"}.to_json
|
||||
expected = {"error" => "Resource not found"}.to_json
|
||||
response.status_code.should eq(404)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
@@ -146,7 +140,7 @@ describe "App::Controllers::Link" do
|
||||
end
|
||||
|
||||
it "should return owned links only" do
|
||||
links = ["https://google.com", "google.com", "google.com.co", "kagi.com"]
|
||||
links = ["https://google.de", "google.de", "google.edu.co", "x.com"]
|
||||
test_user = create_test_user()
|
||||
|
||||
links[0..2].each do |link|
|
||||
@@ -168,7 +162,7 @@ describe "App::Controllers::Link" do
|
||||
it "should return 401 - missing api key" do
|
||||
get "/api/links"
|
||||
|
||||
expected = {"error" => "Unauthorized"}.to_json
|
||||
expected = {"error" => "Unauthorized access"}.to_json
|
||||
response.status_code.should eq(401)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
@@ -176,7 +170,7 @@ describe "App::Controllers::Link" do
|
||||
|
||||
describe "Get" do
|
||||
it "should return the specified link with click details" do
|
||||
link = "https://kagi.com"
|
||||
link = "https://bing.com"
|
||||
test_user = create_test_user()
|
||||
test_link = create_test_link(test_user, link)
|
||||
|
||||
@@ -192,7 +186,7 @@ describe "App::Controllers::Link" do
|
||||
|
||||
get("/api/links/1", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})
|
||||
|
||||
expected = {"error" => "Not Found"}.to_json
|
||||
expected = {"error" => "Resource not found"}.to_json
|
||||
response.status_code.should eq(404)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
@@ -200,7 +194,7 @@ describe "App::Controllers::Link" do
|
||||
it "should return 401 - missing api key" do
|
||||
get "/api/links/1"
|
||||
|
||||
expected = {"error" => "Unauthorized"}.to_json
|
||||
expected = {"error" => "Unauthorized access"}.to_json
|
||||
response.status_code.should eq(401)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
@@ -208,11 +202,11 @@ describe "App::Controllers::Link" do
|
||||
|
||||
describe "Update" do
|
||||
it "should update link url" do
|
||||
link = "https://kagi.com"
|
||||
link = "https://github.com"
|
||||
test_user = create_test_user()
|
||||
test_link = create_test_link(test_user, link)
|
||||
|
||||
payload = {"url" => "https://kagi.com.co"}
|
||||
payload = {"url" => "https://github.com.co"}
|
||||
put(
|
||||
"/api/links/#{test_link.id}",
|
||||
headers: HTTP::Headers{"Content-Type" => "application/json", "X-Api-Key" => test_user.api_key.to_s},
|
||||
@@ -233,7 +227,7 @@ describe "App::Controllers::Link" do
|
||||
body: payload.to_json
|
||||
)
|
||||
|
||||
expected = {"error" => "Not Found"}.to_json
|
||||
expected = {"error" => "Resource not found"}.to_json
|
||||
response.status_code.should eq(404)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
@@ -246,7 +240,7 @@ describe "App::Controllers::Link" do
|
||||
body: payload.to_json
|
||||
)
|
||||
|
||||
expected = {"error" => "Unauthorized"}.to_json
|
||||
expected = {"error" => "Unauthorized access"}.to_json
|
||||
response.status_code.should eq(401)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
@@ -254,7 +248,7 @@ describe "App::Controllers::Link" do
|
||||
|
||||
describe "Delete" do
|
||||
it "should delete link url" do
|
||||
link = "https://kagi.com"
|
||||
link = "https://news.ycombinator.com"
|
||||
test_user = create_test_user()
|
||||
test_link = create_test_link(test_user, link)
|
||||
|
||||
@@ -268,7 +262,7 @@ describe "App::Controllers::Link" do
|
||||
|
||||
delete("/api/links/1", headers: HTTP::Headers{"X-Api-Key" => test_user.api_key.to_s})
|
||||
|
||||
expected = {"error" => "Not Found"}.to_json
|
||||
expected = {"error" => "Resource not found"}.to_json
|
||||
response.status_code.should eq(404)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
@@ -276,7 +270,7 @@ describe "App::Controllers::Link" do
|
||||
it "should return 401 - missing api key" do
|
||||
delete "/api/links/1"
|
||||
|
||||
expected = {"error" => "Unauthorized"}.to_json
|
||||
expected = {"error" => "Unauthorized access"}.to_json
|
||||
response.status_code.should eq(401)
|
||||
response.body.should eq(expected)
|
||||
end
|
||||
|
||||
+16
-4
@@ -1,11 +1,21 @@
|
||||
require "uuid"
|
||||
require "file_utils"
|
||||
|
||||
require "spec-kemal"
|
||||
require "micrate"
|
||||
|
||||
require "dotenv"
|
||||
Dotenv.load ".env.#{ENV["ENV"]}"
|
||||
|
||||
require "../bit"
|
||||
|
||||
Spec.before_suite do
|
||||
# Delete the SQLite database file if it exists
|
||||
db_file_path = ENV["DATABASE_URL"].split("sqlite3://").last.split("?").first
|
||||
if File.exists?(db_file_path)
|
||||
File.delete(db_file_path)
|
||||
end
|
||||
|
||||
Micrate::DB.connection_url = ENV["DATABASE_URL"]
|
||||
Micrate::Cli.run_up
|
||||
|
||||
@@ -20,7 +30,8 @@ def create_test_user
|
||||
|
||||
changeset = App::Lib::Database.insert(user)
|
||||
if !changeset.valid?
|
||||
raise "Test user creation failed"
|
||||
error_messages = changeset.errors.map { |error| "#{error}" }.join(", ")
|
||||
raise "Test user creation failed #{error_messages}"
|
||||
end
|
||||
|
||||
user
|
||||
@@ -29,13 +40,14 @@ end
|
||||
def create_test_link(user, url)
|
||||
link = App::Models::Link.new
|
||||
link.id = UUID.v4.to_s
|
||||
link.slug = App::Services::SlugService.shorten_url(url, user.id.to_s)
|
||||
link.url = url
|
||||
link.slug = Random::Secure.urlsafe_base64(4)
|
||||
link.user = user
|
||||
|
||||
changeset = App::Lib::Database.insert(link)
|
||||
if !changeset.valid?
|
||||
raise "Test link creation failed"
|
||||
unless changeset.valid?
|
||||
error_messages = changeset.errors.map { |error| "#{error}" }.join(", ")
|
||||
raise "Test link creation failed: #{error_messages}"
|
||||
end
|
||||
|
||||
link.clicks = [] of App::Models::Click
|
||||
|
||||
Reference in New Issue
Block a user