Merge pull request #147 from cupcakearmy/actix-2-axum

Actix 2 axum
This commit is contained in:
Nicco 2024-08-27 15:06:29 +02:00 committed by GitHub
commit 6b5eea34a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1198 additions and 1640 deletions

View File

View File

@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Node # Node
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
cache: 'pnpm' cache: 'pnpm'
@ -27,13 +27,16 @@ jobs:
- name: Prepare - name: Prepare
run: | run: |
pnpm install --frozen-lockfile pnpm install
pnpm exec playwright install --with-deps pnpm exec playwright install --with-deps
pnpm run test:prepare pnpm run test:prepare
- name: Run your tests - name: Run your tests
run: pnpm test run: pnpm test
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with: with:
name: test-results name: playwright-report
path: test-results path: test-results/
retention-days: 7

File diff suppressed because it is too large Load Diff

View File

@ -178,7 +178,7 @@ Running `pnpm run dev` in the root folder will start the following things:
- client - client
- cli - cli
You can see the app under [localhost:1234](http://localhost:1234). You can see the app under [localhost:3000](http://localhost:3000).
> There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json) > There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json)

View File

@ -59,19 +59,19 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
## Variables de entorno ## Variables de entorno
| Variable | Default | Descripción | | Variable | Default | Descripción |
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REDIS` | `redis://redis/` | Redis URL a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) | | `REDIS` | `redis://redis/` | Redis URL a la que conectarse. [Según el formato](https://docs.rs/redis/latest/redis/#connection-parameters) |
| `SIZE_LIMIT` | `1 KiB` | Tamaño máximo. Valores aceptados según la [unidad byte](https://docs.rs/byte-unit/). <br> `512 MiB` es el máximo permitido. <br> El frontend mostrará ese número, incluyendo el ~35% de sobrecarga de codificación. | | `SIZE_LIMIT` | `1 KiB` | Tamaño máximo. Valores aceptados según la [unidad byte](https://docs.rs/byte-unit/). <br> `512 MiB` es el máximo permitido. <br> El frontend mostrará ese número, incluyendo el ~35% de sobrecarga de codificación. |
| `MAX_VIEWS` | `100` | Número máximo de vistas. | | `MAX_VIEWS` | `100` | Número máximo de vistas. |
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. | | `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. | | `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. | | `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` | | `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. | | `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. | | `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título | | `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. | | `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
## Despliegue ## Despliegue
@ -169,7 +169,7 @@ Ejecutando `pnpm run dev` en la carpeta raíz iniciará lo siguiente:
- client - client
- cli - cli
Puedes ver la app en [localhost:1234](http://localhost:1234). Puedes ver la app en [localhost:3000](http://localhost:3000).
> Existe una colección de Postman con algunas peticiones de ejemplo [disponible en el repo](./Cryptgeon.postman_collection.json) > Existe una colección de Postman con algunas peticiones de ejemplo [disponible en el repo](./Cryptgeon.postman_collection.json)

View File

@ -14,7 +14,7 @@
<a href=""><img src="./.github/lokalise.png" height="50"> <a href=""><img src="./.github/lokalise.png" height="50">
<br/> <br/>
[EN](README.md) | 简体中文 | [ES](README_ES.md) [EN](README.md) | 简体中文 | [ES](README_ES.md)
## 关于本项目 ## 关于本项目
@ -158,7 +158,7 @@ pnpm run dev
- 无热重载的 rust 后端 - 无热重载的 rust 后端
- 可热重载的客户端 - 可热重载的客户端
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234). 你可以通过 3000 端口进入该应用,即 [localhost:3000](http://localhost:3000).
## 测试 ## 测试

View File

@ -9,12 +9,12 @@ services:
app: app:
build: . build: .
env_file: .dev.env env_file: .env.dev
depends_on: depends_on:
- redis - redis
restart: unless-stopped restart: unless-stopped
ports: ports:
- 1234:8000 - 3000:8000
healthcheck: healthcheck:
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/'] test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/']

View File

@ -5,8 +5,8 @@
"dev": "run-p dev:*", "dev": "run-p dev:*",
"docker:up": "docker compose -f docker-compose.dev.yaml up", "docker:up": "docker compose -f docker-compose.dev.yaml up",
"docker:build": "docker compose -f docker-compose.dev.yaml build", "docker:build": "docker compose -f docker-compose.dev.yaml build",
"test": "playwright test --project chrome firefox safari", "test": "playwright test --project=chrome --project=firefox --project=safari",
"test:local": "playwright test --project chrome", "test:local": "playwright test --project=chrome",
"test:server": "run-s docker:up", "test:server": "run-s docker:up",
"test:prepare": "run-p build docker:build", "test:prepare": "run-p build docker:build",
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build" "build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
@ -17,5 +17,5 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"shelljs": "^0.8.5" "shelljs": "^0.8.5"
}, },
"packageManager": "pnpm@9.8.0" "packageManager": "pnpm@9.9.0"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "2.7.0" version = "2.8.0"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021" edition = "2021"
rust-version = "1.80" rust-version = "1.80"
@ -10,16 +10,18 @@ name = "cryptgeon"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
actix-web = "4" # Core
actix-files = "0.6" axum = "0.7.5"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0.208", features = ["derive"] }
tokio = { version = "1.39.3", features = ["full"] }
tower = "0.5.0"
tower-http = { version = "0.5.2", features = ["full"] }
redis = { version = "0.25.2", features = ["tls-native-tls"] }
# Utility
serde_json = "1" serde_json = "1"
lazy_static = "1" lazy_static = "1"
ring = "0.16" ring = "0.16"
bs62 = "0.1" bs62 = "0.1"
byte-unit = "4" byte-unit = "4"
dotenv = "0.15" dotenv = "0.15"
mime = "0.3"
env_logger = "0.9"
log = "0.4"
redis = { version = "0.25.2", features = ["tls-native-tls"] }

View File

@ -4,7 +4,7 @@
"scripts": { "scripts": {
"dev": "cargo watch -x 'run --bin cryptgeon'", "dev": "cargo watch -x 'run --bin cryptgeon'",
"build": "cargo build --release", "build": "cargo build --release",
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:1234 cargo run", "test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:3000 cargo run",
"test:prepare": "cargo build" "test:prepare": "cargo build"
} }
} }

View File

@ -1,14 +0,0 @@
use actix_web::web;
use crate::health;
use crate::note;
use crate::status;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/api")
.service(note::init())
.service(status::init())
.service(health::init()),
);
}

View File

@ -1,17 +0,0 @@
use actix_files::{Files, NamedFile};
use actix_web::{web, Result};
use crate::config;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(
Files::new("/", config::FRONTEND_PATH.to_string())
.index_file("index.html")
.use_etag(true),
);
}
pub async fn index() -> Result<NamedFile> {
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
Ok(NamedFile::open(index)?)
}

View File

@ -38,10 +38,6 @@ pub static ref ALLOW_FILES: bool = std::env::var("ALLOW_FILES")
.unwrap_or("true".to_string()) .unwrap_or("true".to_string())
.parse() .parse()
.unwrap(); .unwrap();
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
.unwrap_or("true".to_string())
.parse()
.unwrap();
} }
// THEME // THEME
@ -62,4 +58,8 @@ lazy_static! {
.unwrap_or("".to_string()) .unwrap_or("".to_string())
.parse() .parse()
.unwrap(); .unwrap();
pub static ref THEME_NEW_NOTE_NOTICE: bool = std::env::var("THEME_NEW_NOTE_NOTICE")
.unwrap_or("true".to_string())
.parse()
.unwrap();
} }

View File

@ -1,3 +1,10 @@
mod routes; use crate::store;
use axum::http::StatusCode;
pub use routes::*; pub async fn report_health() -> (StatusCode,) {
if store::can_reach_redis() {
return (StatusCode::OK,);
} else {
return (StatusCode::SERVICE_UNAVAILABLE,);
}
}

View File

@ -1,16 +0,0 @@
use actix_web::{get, web, HttpResponse, Responder, Scope};
use crate::store;
#[get("/")]
async fn get_live() -> impl Responder {
if store::can_reach_redis() {
return HttpResponse::Ok();
} else {
return HttpResponse::ServiceUnavailable();
}
}
pub fn init() -> Scope {
web::scope("/live").service(get_live)
}

View File

@ -1,44 +1,67 @@
use actix_web::{ use axum::{
middleware::{self, Logger}, extract::{DefaultBodyLimit, Request},
web::{self}, routing::{delete, get, post},
App, HttpServer, Router, ServiceExt,
}; };
use dotenv::dotenv; use dotenv::dotenv;
use log::error; use tower::Layer;
use tower_http::{
compression::CompressionLayer,
normalize_path::NormalizePathLayer,
services::{ServeDir, ServeFile},
};
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
mod api;
mod client;
mod config; mod config;
mod health; mod health;
mod note; mod note;
mod size;
mod status; mod status;
mod store; mod store;
#[actix_web::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() {
dotenv().ok(); dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or(config::VERBOSITY.as_str()));
if !store::can_reach_redis() { if !store::can_reach_redis() {
error!("cannot reach redis"); println!("cannot reach redis");
panic!("canont reach redis"); panic!("canont reach redis");
} }
return HttpServer::new(|| { let notes_routes = Router::new()
App::new() .route("/", post(note::create))
.wrap(Logger::new("\"%r\" %s %b %T")) .route("/:id", delete(note::delete))
.wrap(middleware::Compress::default()) .route("/:id", get(note::preview));
.wrap(middleware::DefaultHeaders::default()) let health_routes = Router::new().route("/live", get(health::report_health));
.configure(size::init) let status_routes = Router::new().route("/status", get(status::get_status));
.configure(api::init) let api_routes = Router::new()
.configure(client::init) .nest("/notes", notes_routes)
.default_service(web::to(client::index)) .nest("/", health_routes)
}) .nest("/", status_routes);
.bind(config::LISTEN_ADDR.to_string())?
.run() let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
.await; let serve_dir =
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
let app = Router::new()
.nest("/api", api_routes)
.fallback_service(serve_dir)
.layer(DefaultBodyLimit::max(*config::LIMIT))
.layer(
CompressionLayer::new()
.br(true)
.deflate(true)
.gzip(true)
.zstd(true),
);
let app = NormalizePathLayer::trim_trailing_slash().layer(app);
let listener = tokio::net::TcpListener::bind(config::LISTEN_ADDR.to_string())
.await
.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, ServiceExt::<Request>::into_make_service(app))
.await
.unwrap();
} }

View File

@ -12,12 +12,12 @@ pub struct Note {
pub expiration: Option<u32>, pub expiration: Option<u32>,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize)]
pub struct NoteInfo { pub struct NoteInfo {
pub meta: String, pub meta: String,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize)]
pub struct NotePublic { pub struct NotePublic {
pub meta: String, pub meta: String,
pub contents: String, pub contents: String,

View File

@ -1,11 +1,18 @@
use actix_web::{delete, get, post, web, HttpResponse, Responder, Scope}; use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::SystemTime; use std::time::SystemTime;
use crate::config; use crate::config;
use crate::note::{generate_id, Note, NoteInfo, NotePublic}; use crate::note::{generate_id, Note, NoteInfo};
use crate::store; use crate::store;
use super::NotePublic;
pub fn now() -> u32 { pub fn now() -> u32 {
SystemTime::now() SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
@ -13,20 +20,18 @@ pub fn now() -> u32 {
.as_secs() as u32 .as_secs() as u32
} }
#[derive(Serialize, Deserialize)] #[derive(Deserialize)]
struct NotePath { pub struct OneNoteParams {
id: String, id: String,
} }
#[get("/{id}")] pub async fn preview(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
async fn one(path: web::Path<NotePath>) -> impl Responder { let note = store::get(&id);
let p = path.into_inner();
let note = store::get(&p.id);
match note { match note {
Ok(Some(n)) => HttpResponse::Ok().json(NoteInfo { meta: n.meta }), Ok(Some(n)) => (StatusCode::OK, Json(NoteInfo { meta: n.meta })).into_response(),
Ok(None) => HttpResponse::NotFound().finish(), Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
@ -35,13 +40,16 @@ struct CreateResponse {
id: String, id: String,
} }
#[post("/")] pub async fn create(Json(mut n): Json<Note>) -> Response {
async fn create(note: web::Json<Note>) -> impl Responder { // let mut n = note.into_inner();
let mut n = note.into_inner();
let id = generate_id(); let id = generate_id();
let bad_req = HttpResponse::BadRequest().finish(); // let bad_req = HttpResponse::BadRequest().finish();
if n.views == None && n.expiration == None { if n.views == None && n.expiration == None {
return bad_req; return (
StatusCode::BAD_REQUEST,
"At least views or expiration must be set",
)
.into_response();
} }
if !*config::ALLOW_ADVANCED { if !*config::ALLOW_ADVANCED {
n.views = Some(1); n.views = Some(1);
@ -50,7 +58,7 @@ async fn create(note: web::Json<Note>) -> impl Responder {
match n.views { match n.views {
Some(v) => { Some(v) => {
if v > *config::MAX_VIEWS || v < 1 { if v > *config::MAX_VIEWS || v < 1 {
return bad_req; return (StatusCode::BAD_REQUEST, "Invalid views").into_response();
} }
n.expiration = None; // views overrides expiration n.expiration = None; // views overrides expiration
} }
@ -58,8 +66,8 @@ async fn create(note: web::Json<Note>) -> impl Responder {
} }
match n.expiration { match n.expiration {
Some(e) => { Some(e) => {
if e > *config::MAX_EXPIRATION { if e > *config::MAX_EXPIRATION || e < 1 {
return bad_req; return (StatusCode::BAD_REQUEST, "Invalid expiration").into_response();
} }
let expiration = now() + (e * 60); let expiration = now() + (e * 60);
n.expiration = Some(expiration); n.expiration = Some(expiration);
@ -67,38 +75,40 @@ async fn create(note: web::Json<Note>) -> impl Responder {
_ => {} _ => {}
} }
match store::set(&id.clone(), &n.clone()) { match store::set(&id.clone(), &n.clone()) {
Ok(_) => return HttpResponse::Ok().json(CreateResponse { id: id }), Ok(_) => (StatusCode::OK, Json(CreateResponse { id })).into_response(),
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
#[delete("/{id}")] pub async fn delete(Path(OneNoteParams { id }): Path<OneNoteParams>) -> Response {
async fn delete(path: web::Path<NotePath>) -> impl Responder { let note = store::get(&id);
let p = path.into_inner();
let note = store::get(&p.id);
match note { match note {
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), // Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
Ok(None) => return HttpResponse::NotFound().finish(), // Ok(None) => return HttpResponse::NotFound().finish(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
Ok(Some(note)) => { Ok(Some(note)) => {
let mut changed = note.clone(); let mut changed = note.clone();
if changed.views == None && changed.expiration == None { if changed.views == None && changed.expiration == None {
return HttpResponse::BadRequest().finish(); return (StatusCode::BAD_REQUEST).into_response();
} }
match changed.views { match changed.views {
Some(v) => { Some(v) => {
changed.views = Some(v - 1); changed.views = Some(v - 1);
let id = p.id.clone(); let id = id.clone();
if v <= 1 { if v <= 1 {
match store::del(&id) { match store::del(&id) {
Err(e) => { Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string()) return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response();
} }
_ => {} _ => {}
} }
} else { } else {
match store::set(&id, &changed.clone()) { match store::set(&id, &changed.clone()) {
Err(e) => { Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string()) return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response();
} }
_ => {} _ => {}
} }
@ -111,33 +121,26 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
match changed.expiration { match changed.expiration {
Some(e) => { Some(e) => {
if e < n { if e < n {
match store::del(&p.id.clone()) { match store::del(&id.clone()) {
Ok(_) => return HttpResponse::BadRequest().finish(), Ok(_) => return (StatusCode::BAD_REQUEST).into_response(),
Err(e) => { Err(e) => {
return HttpResponse::InternalServerError().body(e.to_string()) return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
.into_response()
} }
} }
} }
} }
_ => {} _ => {}
} }
return HttpResponse::Ok().json(NotePublic {
contents: changed.contents, return (
meta: changed.meta, StatusCode::OK,
}); Json(NotePublic {
contents: changed.contents,
meta: changed.meta,
}),
)
.into_response();
} }
} }
} }
#[derive(Serialize, Deserialize)]
struct Status {
version: String,
max_size: usize,
}
pub fn init() -> Scope {
web::scope("/notes")
.service(one)
.service(create)
.service(delete)
}

View File

@ -1,12 +0,0 @@
use crate::config;
use actix_web::web;
use mime;
pub fn init(cfg: &mut web::ServiceConfig) {
let json = web::JsonConfig::default().limit(*config::LIMIT);
let plain = web::PayloadConfig::default()
.limit(*config::LIMIT)
.mimetype(mime::STAR_STAR);
// cfg.app_data(plain);
cfg.app_data(json).app_data(plain);
}

View File

@ -1,5 +1,39 @@
mod model; use crate::config;
mod routes; use axum::{http::StatusCode, Json};
use serde::Serialize;
pub use model::*; #[derive(Serialize)]
pub use routes::*; pub struct Status {
// General
pub version: String,
// Config
pub max_size: u32,
pub max_views: u32,
pub max_expiration: u32,
pub allow_advanced: bool,
pub allow_files: bool,
// Theme
pub theme_image: String,
pub theme_text: String,
pub theme_page_title: String,
pub theme_favicon: String,
pub theme_new_note_notice: bool,
}
pub async fn get_status() -> (StatusCode, Json<Status>) {
let status = Status {
version: config::VERSION.to_string(),
max_size: *config::LIMIT as u32,
max_views: *config::MAX_VIEWS,
max_expiration: *config::MAX_EXPIRATION,
allow_advanced: *config::ALLOW_ADVANCED,
allow_files: *config::ALLOW_FILES,
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
theme_image: config::THEME_IMAGE.to_string(),
theme_text: config::THEME_TEXT.to_string(),
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
theme_favicon: config::THEME_FAVICON.to_string(),
};
(StatusCode::OK, Json(status))
}

View File

@ -1,19 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Status {
// General
pub version: String,
// Config
pub max_size: u32,
pub max_views: u32,
pub max_expiration: u32,
pub allow_advanced: bool,
pub allow_files: bool,
pub theme_new_note_notice: bool,
// Theme
pub theme_image: String,
pub theme_text: String,
pub theme_page_title: String,
pub theme_favicon: String,
}

View File

@ -1,25 +0,0 @@
use actix_web::{get, web, HttpResponse, Responder, Scope};
use crate::config;
use crate::status::Status;
#[get("/")]
async fn get_status() -> impl Responder {
return HttpResponse::Ok().json(Status {
version: config::VERSION.to_string(),
max_size: *config::LIMIT as u32,
max_views: *config::MAX_VIEWS,
max_expiration: *config::MAX_EXPIRATION,
allow_advanced: *config::ALLOW_ADVANCED,
allow_files: *config::ALLOW_FILES,
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
theme_image: config::THEME_IMAGE.to_string(),
theme_text: config::THEME_TEXT.to_string(),
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
theme_favicon: config::THEME_FAVICON.to_string(),
});
}
pub fn init() -> Scope {
web::scope("/status").service(get_status)
}

View File

@ -1,6 +1,6 @@
{ {
"name": "cryptgeon", "name": "cryptgeon",
"version": "2.7.0", "version": "2.8.0",
"homepage": "https://github.com/cupcakearmy/cryptgeon", "homepage": "https://github.com/cupcakearmy/cryptgeon",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -18,19 +18,19 @@
export let icon: keyof typeof map export let icon: keyof typeof map
</script> </script>
<div on:click {...$$restProps}> <button on:click {...$$restProps}>
{#if map[icon]} {#if map[icon]}
<svelte:component this={map[icon]} /> <svelte:component this={map[icon]} />
{/if} {/if}
</div> </button>
<style> <style>
div { button {
display: inline-block; display: inline-block;
contain: strict; contain: strict;
box-sizing: content-box; box-sizing: content-box;
} }
div > :global(svg) { button > :global(svg) {
display: block; display: block;
fill: currentColor; fill: currentColor;
} }

View File

@ -39,13 +39,23 @@
<input bind:value {...$$restProps} class:valid={valid === true} /> <input bind:value {...$$restProps} class:valid={valid === true} />
<div class="icons"> <div class="icons">
{#if isPassword} {#if isPassword}
<Icon class="icon" icon={hidden ? 'eye' : 'eye-off'} on:click={toggle} /> <Icon
disabled={$$restProps.disabled}
class="icon"
icon={hidden ? 'eye' : 'eye-off'}
on:click={toggle}
/>
{/if} {/if}
{#if random} {#if random}
<Icon class="icon" icon="dice" on:click={randomFN} /> <Icon disabled={$$restProps.disabled} class="icon" icon="dice" on:click={randomFN} />
{/if} {/if}
{#if copy} {#if copy}
<Icon class="icon" icon="copy" on:click={() => copyFN(value.toString())} /> <Icon
disabled={$$restProps.disabled}
class="icon"
icon="copy"
on:click={() => copyFN(value.toString())}
/>
{/if} {/if}
</div> </div>
</label> </label>

View File

@ -12,5 +12,5 @@ const server = http.createServer(function (req, res) {
const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001' const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001'
proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 }) proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 })
}) })
server.listen(1234) server.listen(3000)
console.log('Proxy on http://localhost:1234') console.log('Proxy on http://localhost:3000')

View File

@ -116,6 +116,7 @@ export type Status = {
theme_text: string theme_text: string
theme_favicon: string theme_favicon: string
theme_page_title: string theme_page_title: string
theme_new_note_notice: boolean
} }
export async function status() { export async function status() {

View File

@ -3,18 +3,19 @@ import { devices, type PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
use: { use: {
video: 'retain-on-failure', video: 'retain-on-failure',
baseURL: 'http://localhost:1234', baseURL: 'http://localhost:3000',
actionTimeout: 10_000, actionTimeout: 30_000,
}, },
outputDir: './test-results', outputDir: './test-results',
testDir: './test', testDir: './test',
timeout: 10_000, timeout: 30_000,
fullyParallel: true, fullyParallel: true,
retries: 2,
webServer: { webServer: {
command: 'docker compose -f docker-compose.dev.yaml up', command: 'pnpm run docker:up',
port: 1234, port: 3000,
reuseExistingServer: true, reuseExistingServer: true,
}, },
@ -22,10 +23,6 @@ const config: PlaywrightTestConfig = {
{ name: 'chrome', use: { ...devices['Desktop Chrome'] } }, { name: 'chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'safari', use: { ...devices['Desktop Safari'] } }, { name: 'safari', use: { ...devices['Desktop Safari'] } },
{ name: 'cli', use: { ...devices['Desktop Chrome'] }, grep: [/@cli/] },
{ name: 'web', use: { ...devices['Desktop Chrome'] }, grep: [/@web/] },
{ name: 'cross', use: { ...devices['Desktop Chrome'] }, grep: [/@cross/] },
], ],
} }

View File

@ -1,10 +1,11 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { basename } from 'node:path' import { basename } from 'node:path'
import { Files, getFileChecksum, rm, tmpFile } from '../../files' import { rm } from 'node:fs/promises'
import { Files, getFileChecksum, tmpFile } from '../../files'
import { CLI, getLinkFromCLI } from '../../utils' import { CLI, getLinkFromCLI } from '../../utils'
test.describe('file @cli', () => { test.describe('file @cli', () => {
test('simple', async ({ page }) => { test('simple', async () => {
const file = await tmpFile(Files.Image) const file = await tmpFile(Files.Image)
const checksum = await getFileChecksum(file) const checksum = await getFileChecksum(file)
const note = await CLI('send', 'file', file) const note = await CLI('send', 'file', file)
@ -17,7 +18,7 @@ test.describe('file @cli', () => {
test.expect(checksum).toBe(c) test.expect(checksum).toBe(c)
}) })
test('simple with password', async ({ page }) => { test('simple with password', async () => {
const file = await tmpFile(Files.Image) const file = await tmpFile(Files.Image)
const password = 'password' const password = 'password'
const checksum = await getFileChecksum(file) const checksum = await getFileChecksum(file)

View File

@ -2,7 +2,7 @@ import { test } from '@playwright/test'
import { CLI, getLinkFromCLI } from '../../utils' import { CLI, getLinkFromCLI } from '../../utils'
test.describe('text @cli', () => { test.describe('text @cli', () => {
test('simple', async ({ page }) => { test('simple', async () => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const note = await CLI('send', 'text', text) const note = await CLI('send', 'text', text)
const link = getLinkFromCLI(note.stdout) const link = getLinkFromCLI(note.stdout)
@ -11,7 +11,7 @@ test.describe('text @cli', () => {
test.expect(retrieved.stdout.trim()).toBe(text) test.expect(retrieved.stdout.trim()).toBe(text)
}) })
test('simple with password', async ({ page }) => { test('simple with password', async () => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying.` const text = `Endless prejudice endless play derive joy eternal-return selfish burying.`
const password = 'password' const password = 'password'
const note = await CLI('send', 'text', text, '--password', password) const note = await CLI('send', 'text', text, '--password', password)

View File

@ -1,7 +1,8 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { CLI, checkLinkForDownload, checkLinkForText, createNote, getLinkFromCLI } from '../../utils' import { rm } from 'node:fs/promises'
import { Files, getFileChecksum, rm, tmpFile } from '../../files' import { basename } from 'node:path'
import { basename } from 'path' import { Files, getFileChecksum, tmpFile } from '../../files'
import { CLI, checkLinkForDownload, createNoteSuccessfully, getLinkFromCLI } from '../../utils'
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const password = 'password' const password = 'password'
@ -30,7 +31,7 @@ test.describe('text @cross', () => {
test('web to cli', async ({ page }) => { test('web to cli', async ({ page }) => {
const files = [Files.Image] const files = [Files.Image]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files }) const link = await createNoteSuccessfully(page, { files })
const filename = basename(files[0]) const filename = basename(files[0])
await CLI('open', link, '--all') await CLI('open', link, '--all')
@ -42,7 +43,7 @@ test.describe('text @cross', () => {
test('web to cli with password', async ({ page }) => { test('web to cli with password', async ({ page }) => {
const files = [Files.Image] const files = [Files.Image]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files, password }) const link = await createNoteSuccessfully(page, { files, password })
const filename = basename(files[0]) const filename = basename(files[0])
await CLI('open', link, '--all', '--password', password) await CLI('open', link, '--all', '--password', password)

View File

@ -1,5 +1,5 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { CLI, checkLinkForText, createNote, getLinkFromCLI } from '../../utils' import { CLI, checkLinkForText, createNoteSuccessfully, getLinkFromCLI } from '../../utils'
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const password = 'password' const password = 'password'
@ -13,7 +13,7 @@ test.describe('text @cross', () => {
}) })
test('web to cli', async ({ page }) => { test('web to cli', async ({ page }) => {
const link = await createNote(page, { text }) const link = await createNoteSuccessfully(page, { text })
const retrieved = await CLI('open', link) const retrieved = await CLI('open', link)
test.expect(retrieved.stdout.trim()).toBe(text) test.expect(retrieved.stdout.trim()).toBe(text)
}) })
@ -25,7 +25,7 @@ test.describe('text @cross', () => {
}) })
test('web to cli with password', async ({ page }) => { test('web to cli with password', async ({ page }) => {
const link = await createNote(page, { text, password }) const link = await createNoteSuccessfully(page, { text, password })
const retrieved = await CLI('open', link, '--password', password) const retrieved = await CLI('open', link, '--password', password)
test.expect(retrieved.stdout.trim()).toBe(text) test.expect(retrieved.stdout.trim()).toBe(text)
}) })

View File

@ -1,10 +1,5 @@
import { createHash } from 'crypto' import { createHash } from 'node:crypto'
import { cp as cpFN, rm as rmFN } from 'fs' import { cp, readFile } from 'node:fs/promises'
import { readFile } from 'fs/promises'
import { promisify } from 'util'
export const cp = promisify(cpFN)
export const rm = promisify(rmFN)
export const Files = { export const Files = {
PDF: 'test/assets/AES.pdf', PDF: 'test/assets/AES.pdf',

View File

@ -6,17 +6,22 @@ import { getFileChecksum } from './files'
const exec = promisify(execFile) const exec = promisify(execFile)
type CreatePage = { type CreatePage = {
text?: string
files?: string[]
views?: number views?: number
expiration?: number expiration?: number
error?: string error?: string
password?: string password?: string
} } & (
export async function createNote(page: Page, options: CreatePage): Promise<string> { | {
text: string
}
| {
files: string[]
}
)
async function createNote(page: Page, options: CreatePage): Promise<void> {
await page.goto('/') await page.goto('/')
if (options.text) { if ('text' in options) {
await page.getByTestId('text-field').fill(options.text) await page.getByTestId('text-field').fill(options.text)
} else if (options.files) { } else if (options.files) {
await page.getByTestId('switch-file').click() await page.getByTestId('switch-file').click()
@ -31,7 +36,8 @@ export async function createNote(page: Page, options: CreatePage): Promise<strin
if (options.views || options.expiration || options.password) await page.getByTestId('switch-advanced').click() if (options.views || options.expiration || options.password) await page.getByTestId('switch-advanced').click()
if (options.views) { if (options.views) {
await page.getByTestId('field-views').fill(options.views.toString()) await page.getByTestId('field-views').fill(options.views.toString())
} else if (options.expiration) { }
if (options.expiration) {
await page.getByTestId('switch-advanced-toggle').click() await page.getByTestId('switch-advanced-toggle').click()
await page.getByTestId('field-expiration').fill(options.expiration.toString()) await page.getByTestId('field-expiration').fill(options.expiration.toString())
} }
@ -41,15 +47,18 @@ export async function createNote(page: Page, options: CreatePage): Promise<strin
} }
await page.locator('button:has-text("create")').click() await page.locator('button:has-text("create")').click()
}
if (options.error) { export async function createNoteSuccessfully(page: Page, options: CreatePage): Promise<string> {
await expect(page.locator('.error-text')).toContainText(options.error, { timeout: 60_000 }) await createNote(page, options)
}
// Return share link
return await page.getByTestId('share-link').inputValue() return await page.getByTestId('share-link').inputValue()
} }
export async function createNoteError(page: Page, options: CreatePage, error: string): Promise<void> {
await createNote(page, options)
await expect(page.locator('._toastContainer')).toContainText(error)
}
type CheckLinkBase = { type CheckLinkBase = {
link: string link: string
text: string text: string
@ -69,7 +78,7 @@ export async function checkLinkForDownload(page: Page, options: CheckLinkBase &
const path = await download.path() const path = await download.path()
if (!path) throw new Error('Download failed') if (!path) throw new Error('Download failed')
const cs = await getFileChecksum(path) const cs = await getFileChecksum(path)
await expect(cs).toBe(options.checksum) expect(cs).toBe(options.checksum)
} }
export async function checkLinkForText(page: Page, options: CheckLinkBase) { export async function checkLinkForText(page: Page, options: CheckLinkBase) {
@ -78,7 +87,7 @@ export async function checkLinkForText(page: Page, options: CheckLinkBase) {
if (options.password) await page.getByTestId('show-note-password').fill(options.password) if (options.password) await page.getByTestId('show-note-password').fill(options.password)
await page.getByTestId('show-note-button').click() await page.getByTestId('show-note-button').click()
const text = await page.getByTestId('result').locator('.note').innerText() const text = await page.getByTestId('result').locator('.note').innerText()
await expect(text).toContain(options.text) expect(text).toContain(options.text)
} }
export async function checkLinkDoesNotExist(page: Page, link: string) { export async function checkLinkDoesNotExist(page: Page, link: string) {
@ -91,7 +100,7 @@ export async function CLI(...args: string[]) {
return await exec('./packages/cli/dist/cli.cjs', args, { return await exec('./packages/cli/dist/cli.cjs', args, {
env: { env: {
...process.env, ...process.env,
CRYPTGEON_SERVER: 'http://localhost:1234', CRYPTGEON_SERVER: 'http://localhost:3000',
}, },
}) })
} }

View File

@ -1,12 +1,12 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { Files, getFileChecksum } from '../../files' import { Files, getFileChecksum } from '../../files'
import { checkLinkForDownload, createNote } from '../../utils' import { checkLinkForDownload, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('multiple', async ({ page }) => { test('multiple', async ({ page }) => {
const files = [Files.PDF, Files.Image] const files = [Files.PDF, Files.Image]
const checksums = await Promise.all(files.map(getFileChecksum)) const checksums = await Promise.all(files.map(getFileChecksum))
const link = await createNote(page, { files, views: 2 }) const link = await createNoteSuccessfully(page, { files, views: 2 })
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum: checksums[1] }) await checkLinkForDownload(page, { link, text: 'image.jpg', checksum: checksums[1] })
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum: checksums[0] }) await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum: checksums[0] })
}) })

View File

@ -1,11 +1,11 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { Files, getFileChecksum } from '../../files' import { Files, getFileChecksum } from '../../files'
import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote } from '../../utils' import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('simple pdf', async ({ page }) => { test('simple pdf', async ({ page }) => {
const files = [Files.PDF] const files = [Files.PDF]
const link = await createNote(page, { files }) const link = await createNoteSuccessfully(page, { files })
await checkLinkForText(page, { link, text: 'AES.pdf' }) await checkLinkForText(page, { link, text: 'AES.pdf' })
await checkLinkDoesNotExist(page, link) await checkLinkDoesNotExist(page, link)
}) })
@ -13,21 +13,21 @@ test.describe('@web', () => {
test('pdf content', async ({ page }) => { test('pdf content', async ({ page }) => {
const files = [Files.PDF] const files = [Files.PDF]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files }) const link = await createNoteSuccessfully(page, { files })
await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum }) await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum })
}) })
test('image content', async ({ page }) => { test('image content', async ({ page }) => {
const files = [Files.Image] const files = [Files.Image]
const checksum = await getFileChecksum(files[0]) const checksum = await getFileChecksum(files[0])
const link = await createNote(page, { files }) const link = await createNoteSuccessfully(page, { files })
await checkLinkForDownload(page, { link, text: 'image.jpg', checksum }) await checkLinkForDownload(page, { link, text: 'image.jpg', checksum })
}) })
test('simple pdf with password', async ({ page }) => { test('simple pdf with password', async ({ page }) => {
const files = [Files.PDF] const files = [Files.PDF]
const password = 'password' const password = 'password'
const link = await createNote(page, { files, password }) const link = await createNoteSuccessfully(page, { files, password })
await checkLinkForText(page, { link, text: 'AES.pdf', password }) await checkLinkForText(page, { link, text: 'AES.pdf', password })
await checkLinkDoesNotExist(page, link) await checkLinkDoesNotExist(page, link)
}) })

View File

@ -1,10 +1,10 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { createNote } from '../../utils'
import { Files } from '../../files' import { Files } from '../../files'
import { createNoteError } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test.skip('to big zip', async ({ page }) => { test.skip('to big zip', async ({ page }) => {
const files = [Files.Zip] const files = [Files.Zip]
const link = await createNote(page, { files, error: 'note is to big' }) await createNoteError(page, { files }, 'could not create note. note is too big')
}) })
}) })

View File

@ -1,13 +1,14 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils' import { checkLinkDoesNotExist, checkLinkForText, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('1 minute', async ({ page }) => { test('1 minute', async ({ page, browserName }) => {
test.skip(browserName === 'webkit')
const text = `Virtues value ascetic revaluation sea dead strong burying.` const text = `Virtues value ascetic revaluation sea dead strong burying.`
const minutes = 1 const minutes = 1
const timeout = minutes * 60_000 const timeout = minutes * 60_000
test.setTimeout(timeout * 2) test.setTimeout(timeout * 2)
const link = await createNote(page, { text, expiration: minutes }) const link = await createNoteSuccessfully(page, { text, expiration: minutes })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await page.waitForTimeout(timeout) await page.waitForTimeout(timeout)

View File

@ -1,17 +1,17 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { checkLinkForText, createNote } from '../../utils' import { checkLinkForText, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('simple', async ({ page }) => { test('simple', async ({ page }) => {
const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of deceive play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of deceive play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.`
const link = await createNote(page, { text }) const link = await createNoteSuccessfully(page, { text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
}) })
test('simple with password', async ({ page }) => { test('simple with password', async ({ page }) => {
const text = 'Foo bar' const text = 'Foo bar'
const password = '123' const password = '123'
const shareLink = await createNote(page, { text, password }) const shareLink = await createNoteSuccessfully(page, { text, password })
await checkLinkForText(page, { link: shareLink, text, password }) await checkLinkForText(page, { link: shareLink, text, password })
}) })
}) })

View File

@ -1,17 +1,17 @@
import { test } from '@playwright/test' import { test } from '@playwright/test'
import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils' import { checkLinkDoesNotExist, checkLinkForText, createNoteSuccessfully } from '../../utils'
test.describe('@web', () => { test.describe('@web', () => {
test('only shown once', async ({ page }) => { test('only shown once', async ({ page }) => {
const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.` const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.`
const link = await createNote(page, { text }) const link = await createNoteSuccessfully(page, { text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await checkLinkDoesNotExist(page, link) await checkLinkDoesNotExist(page, link)
}) })
test('view 3 times', async ({ page }) => { test('view 3 times', async ({ page }) => {
const text = `Justice holiest overcome fearful strong ultimate holiest christianity.` const text = `Justice holiest overcome fearful strong ultimate holiest christianity.`
const link = await createNote(page, { text, views: 3 }) const link = await createNoteSuccessfully(page, { text, views: 3 })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })
await checkLinkForText(page, { link, text }) await checkLinkForText(page, { link, text })