From 8d1295bd235dba35450ed02f9300b503b8a10e8d Mon Sep 17 00:00:00 2001 From: Ada Date: Thu, 25 Jan 2024 17:58:55 +0100 Subject: [PATCH] :recycle: Move code from main to module & clean code --- .env | 7 +- .gitignore | 4 + config.go | 54 ----------- db.go | 49 ---------- go.mod | 4 +- internal/config/config.go | 59 ++++++++++++ internal/constant/constants.go | 7 ++ internal/database/db.go | 64 +++++++++++++ internal/httpServer/server.go | 51 +++++++++++ internal/utils/error.go | 17 ++++ utils.go => internal/utils/utils.go | 50 +++++++---- internal/web/plak/error.go | 10 +++ internal/web/plak/plak.go | 134 ++++++++++++++++++++++++++++ internal/web/static/static.go | 15 ++++ main.go | 112 +++-------------------- static/index.html | 64 ++++++------- static/style.css | 90 ++++++++----------- test/utils/utils_test.go | 81 +++++++++++++++++ utils_test.go | 43 --------- 19 files changed, 560 insertions(+), 355 deletions(-) delete mode 100644 config.go delete mode 100644 db.go create mode 100644 internal/config/config.go create mode 100644 internal/constant/constants.go create mode 100644 internal/database/db.go create mode 100644 internal/httpServer/server.go create mode 100644 internal/utils/error.go rename utils.go => internal/utils/utils.go (52%) create mode 100644 internal/web/plak/error.go create mode 100644 internal/web/plak/plak.go create mode 100644 internal/web/static/static.go create mode 100644 test/utils/utils_test.go delete mode 100644 utils_test.go diff --git a/.env b/.env index 0e99fe4..a7c0978 100644 --- a/.env +++ b/.env @@ -1,7 +1,6 @@ -PLAKKEN_INTERFACE=0.0.0.0 -PLAKKEN_PORT=3000 -PLAKKEN_REDIS_ADDR=localhost:6379 +PLAKKEN_LISTEN=:3000 +PLAKKEN_REDIS_ADDRESS=localhost:6379 PLAKKEN_REDIS_USER= PLAKKEN_REDIS_PASSWORD= PLAKKEN_REDIS_DB=0 -PLAKKEN_REDIS_URL_LEN=5 +PLAKKEN_URL_LENGTH=5 diff --git a/.gitignore b/.gitignore index cb9b7ef..087b7b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# IDE specific +.idea +.vscode + ### Go ### *.exe *.exe~ diff --git a/config.go b/config.go deleted file mode 100644 index 8630a3a..0000000 --- a/config.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "github.com/joho/godotenv" - "log" - "os" - "strconv" -) - -type Config struct { - host string - port string - redisAddr string - redisUser string - redisPassword string - redisDB int - urlLength int -} - -func GetConfig() Config { - err := godotenv.Load() - if err != nil { - log.Fatalf("Error loading .env file: %v", err) - } - - port := os.Getenv("PLAKKEN_PORT") - redisAddr := os.Getenv("PLAKKEN_REDIS_ADDR") - db := os.Getenv("PLAKKEN_REDIS_DB") - uLen := os.Getenv("PLAKKEN_REDIS_URL_LEN") - - if port == "" || redisAddr == "" { - log.Fatal("Missing or invalid PLAKKEN_PORT or PLAKKEN_REDIS_ADDR") - } - - redisDB, err := strconv.Atoi(db) - if err != nil { - log.Fatal("Invalid PLAKKEN_REDIS_DB") - } - - urlLen, err := strconv.Atoi(uLen) - if err != nil { - log.Fatal("Invalid PLAKKEN_REDIS_URL_LEN") - } - - return Config{ - host: os.Getenv("PLAKKEN_INTERFACE"), - port: port, - redisAddr: redisAddr, - redisUser: os.Getenv("PLAKKEN_REDIS_USER"), - redisPassword: os.Getenv("PLAKKEN_REDIS_PASSWORD"), - redisDB: redisDB, - urlLength: urlLen, - } -} diff --git a/db.go b/db.go deleted file mode 100644 index aaaa0ad..0000000 --- a/db.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "context" - "log" - "time" - - "github.com/redis/go-redis/v9" -) - -var ctx = context.Background() - -func ConnectDB() *redis.Client { - localDb := redis.NewClient(&redis.Options{ - Addr: currentConfig.redisAddr, - Username: currentConfig.redisUser, - Password: currentConfig.redisPassword, - DB: currentConfig.redisDB, - }) - return localDb -} - -func insertPaste(key string, content string, secret string, ttl time.Duration) { - type dbSchema struct { - content string - secret string - } - - hash := dbSchema{ - content: content, - secret: secret, - } - err := db.HSet(ctx, key, "content", hash.content) - if err != nil { - log.Println(err) - } - err = db.HSet(ctx, key, "secret", hash.secret) - if ttl > -1 { - db.Expire(ctx, key, ttl) - } -} - -func getContent(key string) string { - return db.HGet(ctx, key, "content").Val() -} - -func DeleteContent(key string) { - db.Del(ctx, key) -} diff --git a/go.mod b/go.mod index 5e80fb0..042b6ec 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module plakken +module git.gnous.eu/gnouseu/plakken -go 1.21 +go 1.22 require ( github.com/joho/godotenv v1.5.1 diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2576b54 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,59 @@ +package config + +import ( + "log" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +// InitConfig Structure for program initialisation +type InitConfig struct { + ListenAddress string + RedisAddress string + RedisUser string + RedisPassword string + RedisDB int + UrlLength uint8 +} + +// GetConfig Initialise configuration form .env +func GetConfig() InitConfig { + err := godotenv.Load() + if err != nil { + log.Fatalf("Error loading .env file: %v", err) + } + + listenAddress := os.Getenv("PLAKKEN_LISTEN") + redisAddress := os.Getenv("PLAKKEN_REDIS_ADDRESS") + db := os.Getenv("PLAKKEN_REDIS_DB") + uLen := os.Getenv("PLAKKEN_URL_LENGTH") + + if listenAddress == "" || redisAddress == "" { + log.Fatal("Missing or invalid listenAddress or PLAKKEN_REDIS_ADDRESS") + } + + redisDB, err := strconv.Atoi(db) + if err != nil { + log.Fatal("Invalid PLAKKEN_REDIS_DB") + } + + urlLength, err := strconv.Atoi(uLen) + if err != nil { + log.Fatal("Invalid PLAKKEN_URL_LENGTH") + } + + if urlLength > 255 { + log.Fatal("PLAKKEN_URL_LENGTH cannot be greater than 255") + } + + return InitConfig{ + ListenAddress: listenAddress, + RedisAddress: redisAddress, + RedisUser: os.Getenv("PLAKKEN_REDIS_USER"), + RedisPassword: os.Getenv("PLAKKEN_REDIS_PASSWORD"), + RedisDB: redisDB, + UrlLength: uint8(urlLength), + } +} diff --git a/internal/constant/constants.go b/internal/constant/constants.go new file mode 100644 index 0000000..0522e19 --- /dev/null +++ b/internal/constant/constants.go @@ -0,0 +1,7 @@ +package constant + +import "time" + +const ( + HTTPTimeout = 3 * time.Second +) diff --git a/internal/database/db.go b/internal/database/db.go new file mode 100644 index 0000000..1769dc0 --- /dev/null +++ b/internal/database/db.go @@ -0,0 +1,64 @@ +package database + +import ( + "context" + "log" + "time" + + "github.com/redis/go-redis/v9" +) + +type DBConfig struct { + DB *redis.Client +} + +var ctx = context.Background() + +// InitDB initialise redis connection settings +func InitDB(addr string, user string, password string, db int) *redis.Options { + DBConfig := &redis.Options{ + Addr: addr, + Username: user, + Password: password, + DB: db, + } + + return DBConfig +} + +// ConnectDB make new database connection +func ConnectDB(dbConfig *redis.Options) *redis.Client { + localDb := redis.NewClient(dbConfig) + return localDb +} + +func (config DBConfig) InsertPaste(key string, content string, secret string, ttl time.Duration) { + type dbSchema struct { + content string + secret string + } + + hash := dbSchema{ + content: content, + secret: secret, + } + err := config.DB.HSet(ctx, key, "content", hash.content).Err() + if err != nil { + log.Println(err) + } + err = config.DB.HSet(ctx, key, "secret", hash.secret).Err() + if err != nil { + log.Println(err) + } + if ttl > -1 { + config.DB.Expire(ctx, key, ttl) + } +} + +func (config DBConfig) UrlExist(url string) bool { + return config.DB.Exists(ctx, url).Val() == 1 +} + +func (config DBConfig) VerifySecret(url string, secret string) bool { + return secret == config.DB.HGet(ctx, url, "secret").Val() +} diff --git a/internal/httpServer/server.go b/internal/httpServer/server.go new file mode 100644 index 0000000..3c4fa52 --- /dev/null +++ b/internal/httpServer/server.go @@ -0,0 +1,51 @@ +package httpServer + +import ( + "log" + "net/http" + + "git.gnous.eu/gnouseu/plakken/internal/constant" + "git.gnous.eu/gnouseu/plakken/internal/web/plak" + "git.gnous.eu/gnouseu/plakken/internal/web/static" + "github.com/redis/go-redis/v9" +) + +type ServerConfig struct { + HTTPServer *http.Server + UrlLength uint8 + DB *redis.Client +} + +// Configure HTTP router +func (config ServerConfig) router(_ http.ResponseWriter, _ *http.Request) { + WebConfig := plak.WebConfig{ + DB: config.DB, + UrlLength: config.UrlLength, + } + + http.HandleFunc("GET /{$}", static.Home) + http.HandleFunc("GET /{key}/{settings...}", WebConfig.View) + http.HandleFunc("GET /static/{file}", static.ServeStatic) + http.HandleFunc("POST /{$}", WebConfig.Create) + http.HandleFunc("DELETE /{key}", WebConfig.Delete) +} + +// Config Configure HTTP server +func Config(listenAddress string) *http.Server { + server := &http.Server{ + Addr: listenAddress, + ReadTimeout: constant.HTTPTimeout, + WriteTimeout: constant.HTTPTimeout, + } + + return server +} + +// Server Start HTTP server +func (config ServerConfig) Server() { + log.Println("Listening on " + config.HTTPServer.Addr) + + http.HandleFunc("/", config.router) + + log.Fatal(config.HTTPServer.ListenAndServe()) +} diff --git a/internal/utils/error.go b/internal/utils/error.go new file mode 100644 index 0000000..d946d64 --- /dev/null +++ b/internal/utils/error.go @@ -0,0 +1,17 @@ +package utils + +type ParseIntBeforeSeparatorError struct { + Message string +} + +func (m *ParseIntBeforeSeparatorError) Error() string { + return "parseIntBeforeSeparator: " + m.Message +} + +type ParseExpirationError struct { + Message string +} + +func (m *ParseExpirationError) Error() string { + return "parseIntBeforeSeparator: " + m.Message +} diff --git a/utils.go b/internal/utils/utils.go similarity index 52% rename from utils.go rename to internal/utils/utils.go index 5d23dce..77d41fd 100644 --- a/utils.go +++ b/internal/utils/utils.go @@ -1,18 +1,18 @@ -package main +package utils import ( "crypto/rand" "encoding/hex" - "fmt" "log" mathrand "math/rand" "strconv" "strings" ) -func GenerateUrl() string { +// GenerateUrl generate random string for plak url +func GenerateUrl(length uint8) string { listChars := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - b := make([]rune, currentConfig.urlLength) + b := make([]rune, length) for i := range b { b[i] = listChars[mathrand.Intn(len(listChars))] } @@ -20,6 +20,7 @@ func GenerateUrl() string { return string(b) } +// GenerateSecret generate random secret (32 bytes hexadecimal) func GenerateSecret() string { key := make([]byte, 32) _, err := rand.Read(key) @@ -30,62 +31,73 @@ func GenerateSecret() string { return hex.EncodeToString(key) } -func UrlExist(url string) bool { - return db.Exists(ctx, url).Val() == 1 +// CheckCharRedundant verify is a character is redundant in a string +func CheckCharRedundant(source string, char string) bool { // Verify if a char is redundant + if strings.Count(source, char) > 1 { + return true + } + return false } -func VerifySecret(url string, secret string) bool { - return secret == db.HGet(ctx, url, "secret").Val() -} - -func parseIntBeforeSeparator(source *string, sep string) (int, error) { // return -1 if error, only accept positive number +func parseIntBeforeSeparator(source *string, sep string) (int, error) { // return 0 & error if error, only accept positive number + if CheckCharRedundant(*source, sep) { + return 0, &ParseIntBeforeSeparatorError{Message: *source + ": cannot parse value as int"} + } var value int var err error if strings.Contains(*source, sep) { value, err = strconv.Atoi(strings.Split(*source, sep)[0]) if err != nil { log.Println(err) - return -1, fmt.Errorf("parseIntBeforeSeparator : \"%s\" : cannot parse value as int", *source) + return 0, &ParseIntBeforeSeparatorError{Message: *source + ": cannot parse value as int"} } if value < 0 { // Only positive value is correct - return -1, fmt.Errorf("parseIntBeforeSeparator : \"%s\" : format only take positive value", *source) + return 0, &ParseIntBeforeSeparatorError{Message: *source + ": format only take positive value"} } + + if value > 99 { + return 0, &ParseIntBeforeSeparatorError{Message: *source + ": Format only take two number"} + } + *source = strings.Join(strings.Split(*source, sep)[1:], "") } return value, nil } -func ParseExpiration(source string) (int, error) { // return -1 if error +// ParseExpiration Parse "1d1h1m1s" duration format. Return 0 & error if error +func ParseExpiration(source string) (int, error) { var expiration int var tempOutput int var err error - errMessage := "ParseExpiration : \"%s\" : invalid syntax" if source == "0" { return 0, nil } + + source = strings.ToLower(source) + tempOutput, err = parseIntBeforeSeparator(&source, "d") expiration = tempOutput * 86400 if err != nil { log.Println(err) - return -1, fmt.Errorf(errMessage, source) + return 0, &ParseExpirationError{Message: "Invalid syntax"} } tempOutput, err = parseIntBeforeSeparator(&source, "h") expiration += tempOutput * 3600 if err != nil { log.Println(err) - return -1, fmt.Errorf(errMessage, source) + return 0, &ParseExpirationError{Message: "Invalid syntax"} } tempOutput, err = parseIntBeforeSeparator(&source, "m") expiration += tempOutput * 60 if err != nil { log.Println(err) - return -1, fmt.Errorf(errMessage, source) + return 0, &ParseExpirationError{Message: "Invalid syntax"} } tempOutput, err = parseIntBeforeSeparator(&source, "s") expiration += tempOutput * 1 if err != nil { log.Println(err) - return -1, fmt.Errorf(errMessage, source) + return 0, &ParseExpirationError{Message: "Invalid syntax"} } return expiration, nil diff --git a/internal/web/plak/error.go b/internal/web/plak/error.go new file mode 100644 index 0000000..e2241f0 --- /dev/null +++ b/internal/web/plak/error.go @@ -0,0 +1,10 @@ +package plak + +type DeletePlakError struct { + Name string + Err error +} + +func (m *DeletePlakError) Error() string { + return "Cannot delete: " + m.Name + " : " + m.Err.Error() +} diff --git a/internal/web/plak/plak.go b/internal/web/plak/plak.go new file mode 100644 index 0000000..d854a75 --- /dev/null +++ b/internal/web/plak/plak.go @@ -0,0 +1,134 @@ +package plak + +import ( + "context" + "io" + "log" + "net/http" + "time" + + "git.gnous.eu/gnouseu/plakken/internal/database" + "git.gnous.eu/gnouseu/plakken/internal/utils" + "github.com/redis/go-redis/v9" + + "html/template" +) + +var ctx = context.Background() + +type WebConfig struct { + DB *redis.Client + UrlLength uint8 +} + +// Plak "Object" for plak +type Plak struct { + Key string + Content string + Expiration time.Duration + DB *redis.Client +} + +// Create manage POST request for create Plak +func (config WebConfig) Create(w http.ResponseWriter, r *http.Request) { + content := r.FormValue("content") + if content == "" { + w.WriteHeader(http.StatusBadRequest) + } + + dbConf := database.DBConfig{ + DB: config.DB, + } + + secret := utils.GenerateSecret() + key := utils.GenerateUrl(config.UrlLength) + rawExpiration := r.FormValue("exp") + expiration, err := utils.ParseExpiration(rawExpiration) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + } else if expiration == 0 { + dbConf.InsertPaste(key, content, secret, -1) + } else { + dbConf.InsertPaste(key, content, secret, time.Duration(expiration*int(time.Second))) + } + + http.Redirect(w, r, key, http.StatusSeeOther) +} + +// View for plak +func (config WebConfig) View(w http.ResponseWriter, r *http.Request) { + dbConf := database.DBConfig{ + DB: config.DB, + } + var plak Plak + key := r.PathValue("key") + + if dbConf.UrlExist(key) { + plak = Plak{ + Key: key, + DB: config.DB, + } + plak = plak.GetContent() + if r.PathValue("settings") == "raw" { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + _, err := io.WriteString(w, plak.Content) + if err != nil { + log.Println(err) + } + } else { + t, err := template.ParseFiles("templates/paste.html") + if err != nil { + log.Println(err) + } + err = t.Execute(w, plak) + if err != nil { + log.Println(err) + } + } + } else { + w.WriteHeader(http.StatusNotFound) + } +} + +// Delete manage plak deletion endpoint +func (config WebConfig) Delete(w http.ResponseWriter, r *http.Request) { + dbConf := database.DBConfig{ + DB: config.DB, + } + key := r.PathValue("key") + + if dbConf.UrlExist(key) { + secret := r.URL.Query().Get("secret") + if dbConf.VerifySecret(key, secret) { + plak := Plak{ + Key: key, + DB: config.DB, + } + err := plak.deletePlak() + if err != nil { + log.Println(err) + } + w.WriteHeader(http.StatusNoContent) + } else { + w.WriteHeader(http.StatusForbidden) + } + } + w.WriteHeader(http.StatusNotFound) +} + +// deletePlak Delete plak from database +func (plak Plak) deletePlak() error { + err := plak.DB.Del(ctx, plak.Key).Err() + if err != nil { + log.Println(err) + return &DeletePlakError{Name: plak.Key, Err: err} + } + + return nil +} + +// GetContent get plak content +func (plak Plak) GetContent() Plak { + plak.Content = plak.DB.HGet(ctx, plak.Key, "content").Val() + return plak +} diff --git a/internal/web/static/static.go b/internal/web/static/static.go new file mode 100644 index 0000000..4c4a059 --- /dev/null +++ b/internal/web/static/static.go @@ -0,0 +1,15 @@ +package static + +import ( + "net/http" +) + +// ServeStatic Serve static file from static +func ServeStatic(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/"+r.PathValue("file")) // TODO: vérifier si c'est safe +} + +// Home Serve index.html +func Home(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./static/index.html") +} diff --git a/main.go b/main.go index d75aa4b..6ced2da 100644 --- a/main.go +++ b/main.go @@ -1,111 +1,21 @@ package main import ( - "fmt" - "github.com/redis/go-redis/v9" - "html/template" - "io" - "log" - "net/http" - "strings" - "time" + "git.gnous.eu/gnouseu/plakken/internal/config" + "git.gnous.eu/gnouseu/plakken/internal/database" + "git.gnous.eu/gnouseu/plakken/internal/httpServer" ) -var currentConfig Config -var db *redis.Client - -type pasteView struct { - Content string - Key string -} - -func handleRequest(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - clearPath := strings.ReplaceAll(r.URL.Path, "/raw", "") - staticResource := "/static/" - switch r.Method { - case "GET": - if path == "/" { - http.ServeFile(w, r, "./static/index.html") - - } else if strings.HasPrefix(path, staticResource) { - fs := http.FileServer(http.Dir("./static")) - http.Handle(staticResource, http.StripPrefix(staticResource, fs)) - } else { - if UrlExist(clearPath) { - if strings.HasSuffix(path, "/raw") { - pasteContent := getContent(clearPath) - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - _, err := io.WriteString(w, pasteContent) - if err != nil { - log.Println(err) - } - } else { - pasteContent := getContent(path) - s := pasteView{Content: pasteContent, Key: strings.TrimPrefix(path, "/")} - t, err := template.ParseFiles("templates/paste.html") - if err != nil { - log.Println(err) - } - err = t.Execute(w, s) - if err != nil { - log.Println(err) - } - } - } else { - w.WriteHeader(http.StatusNotFound) - } - } - case "POST": - if path == "/" { - secret := GenerateSecret() - url := "/" + GenerateUrl() - content := r.FormValue("content") - rawExpiration := r.FormValue("exp") - expiration, err := ParseExpiration(rawExpiration) - if err != nil { - log.Println(err) - } else if expiration == 0 { - insertPaste(url, content, secret, -1) - } else if expiration == -1 { - w.WriteHeader(http.StatusBadRequest) - } else { - insertPaste(url, content, secret, time.Duration(expiration*int(time.Second))) - } - - http.Redirect(w, r, url, http.StatusSeeOther) - } else { - w.WriteHeader(http.StatusMethodNotAllowed) - } - case "DELETE": - if UrlExist(path) { - secret := r.URL.Query().Get("secret") - if VerifySecret(path, secret) { - DeleteContent(path) - w.WriteHeader(http.StatusNoContent) - } else { - w.WriteHeader(http.StatusForbidden) - } - } else { - w.WriteHeader(http.StatusNotFound) - } - } -} - func main() { - db = ConnectDB() - currentConfig = GetConfig() - listen := currentConfig.host + ":" + currentConfig.port - http.HandleFunc("/", handleRequest) + initConfig := config.GetConfig() + dbConfig := database.InitDB(initConfig.RedisAddress, initConfig.RedisUser, initConfig.RedisPassword, initConfig.RedisDB) + db := database.ConnectDB(dbConfig) - if currentConfig.host == "" { - fmt.Println("Listening on port " + listen) - } else { - fmt.Println("Listening on " + listen) + serverConfig := httpServer.ServerConfig{ + HTTPServer: httpServer.Config(initConfig.ListenAddress), + UrlLength: initConfig.UrlLength, + DB: db, } - err := http.ListenAndServe(listen, nil) - if err != nil { - log.Fatal(err) - } + serverConfig.Server() } diff --git a/static/index.html b/static/index.html index dfaa289..5061a27 100644 --- a/static/index.html +++ b/static/index.html @@ -7,9 +7,8 @@ name="description"> - New plak • Plakken + New plak • Plakken -
@@ -19,43 +18,46 @@
- + diff --git a/static/style.css b/static/style.css index 4005e5a..cf339b3 100644 --- a/static/style.css +++ b/static/style.css @@ -1,8 +1,9 @@ :root { --accent: #be0560; - --background: #141414; + --background: #121212; --border: #333; - --text: #e9e9e9; + --text: #e6e6e6; + --placeholder: #666; } body { @@ -17,6 +18,32 @@ form { flex-flow: row wrap; } +#lines { + color: var(--placeholder); + font: 400 14px/1.6 "JetBrains Mono", monospace; + height: calc(100vh - 29px); + overflow-y: hidden; + padding: 8px 0; + text-align: center; + user-select: none; + width: 26px; +} + +input, select { + background-color: var(--background); + border: 2px solid var(--border); + border-radius: 2px; + color: var(--text); + font-size: 13px; + outline: none; + padding: 6px 8px; + transition: border .15s ease; +} + +input:hover, select:hover { + border-color: var(--text); +} + button, textarea { background-color: inherit; border: none; @@ -24,36 +51,20 @@ button, textarea { resize: none; } -#lines { - color: #999; - font: 400 14px/1.6 "JetBrains Mono", monospace; - height: calc(100vh - 3rem); - overflow-y: hidden; - padding: 10px 0; - text-align: center; - user-select: none; - width: 26px; -} - #content { color: var(--text); font: 400 14px/1.6 "JetBrains Mono", monospace; - height: calc(100vh - 3rem); - padding: 10px 10px 10px 6px; - width: calc(100vw - 42px); -} - -pre { - margin: 15px; + height: calc(100vh - 29px); + padding: 8px; + width: calc(100vw - 45px); } nav { - align-items: end; - bottom: 1rem; + top: 1rem; display: flex; flex-flow: row wrap; position: absolute; - right: 1rem; + right: 12px; } ul { @@ -65,33 +76,12 @@ ul { padding: 0 1.9rem; } -label { - display: block; -} - -input, select { - background-color: var(--background); - border: 2px solid var(--border); - border-radius: 2px; - color: var(--text); - font-size: 15px; - outline: none; - padding: 6px 8px; - transition: border .15s ease; -} - -input:hover, select:hover { - border-color: var(--text); -} - svg { - fill: none; - height: 26px; - stroke: var(--text); - stroke-width: 2; - stroke-linecap: round; + cursor: pointer; + height: 24px; + fill: var(--text); transition: .15s ease; - width: 26px; + width: 24px; } svg:hover { @@ -102,10 +92,6 @@ input:focus-visible, select:focus-visible { border: 2px solid var(--text); } -button:focus-visible { - outline: none; -} - ::selection { background-color: var(--accent); color: #fff; diff --git a/test/utils/utils_test.go b/test/utils/utils_test.go new file mode 100644 index 0000000..1752121 --- /dev/null +++ b/test/utils/utils_test.go @@ -0,0 +1,81 @@ +package utils_test + +import ( + "errors" + "testing" + + "git.gnous.eu/gnouseu/plakken/internal/utils" +) + +func TestCheckCharNotRedundantTrue(t *testing.T) { // Test CheckCharRedundant with redundant char + want := true + got := utils.CheckCharRedundant("2d1h3md4h7s", "h") + if got != want { + t.Fatal("Error in parseExpirationFull, want : ", want, "got : ", got) + } +} + +func TestCheckCharNotRedundantFalse(t *testing.T) { // Test CheckCharRedundant with not redundant char + want := false + got := utils.CheckCharRedundant("2d1h3m47s", "h") + if got != want { + t.Fatal("Error in parseExpirationFull, want : ", want, "got : ", got) + } +} + +func TestParseExpirationFull(t *testing.T) { // test parseExpirationFull with all valid separator + result, _ := utils.ParseExpiration("2d1h3m47s") + correctValue := 176627 + if result != correctValue { + t.Fatal("Error in parseExpirationFull, want : ", correctValue, "got : ", result) + } +} + +func TestParseExpirationMissing(t *testing.T) { // test parseExpirationFull with all valid separator + result, _ := utils.ParseExpiration("1h47s") + correctValue := 3647 + if result != correctValue { + t.Fatal("Error in ParseExpirationFull, want : ", correctValue, "got : ", result) + } +} + +func TestParseExpirationWithCaps(t *testing.T) { // test parseExpirationFull with all valid separator + result, _ := utils.ParseExpiration("2D1h3M47s") + correctValue := 176627 + if result != correctValue { + t.Fatal("Error in parseExpirationFull, want : ", correctValue, "got : ", result) + } +} + +func TestParseExpirationNull(t *testing.T) { // test ParseExpirationFull with all valid separator + result, _ := utils.ParseExpiration("0") + correctValue := 0 + if result != correctValue { + t.Fatal("Error in ParseExpirationFull, want: ", correctValue, "got: ", result) + } +} + +func TestParseExpirationNegative(t *testing.T) { // test ParseExpirationFull with all valid separator + _, got := utils.ParseExpiration("-42h1m4s") + want := &utils.ParseExpirationError{} + if !errors.As(got, &want) { + t.Fatal("Error in ParseExpirationFull, want : ", want, "got : ", got) + } +} + +func TestParseExpirationInvalid(t *testing.T) { // test ParseExpirationFull with all valid separator + _, got := utils.ParseExpiration("8h42h1m1d4s") + want := &utils.ParseExpirationError{} + if !errors.As(got, &want) { + t.Fatal("Error in ParseExpirationFull, want : ", want, "got : ", got) + } + +} + +func TestParseExpirationInvalidRedundant(t *testing.T) { // test ParseExpirationFull with all valid separator + _, got := utils.ParseExpiration("8h42h1m1h4s") + want := &utils.ParseExpirationError{} + if !errors.As(got, &want) { + t.Fatal("Error in ParseExpirationFull, want : ", want, "got : ", got) + } +} diff --git a/utils_test.go b/utils_test.go deleted file mode 100644 index 4beac4c..0000000 --- a/utils_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import "testing" - -func TestParseExpirationFull(t *testing.T) { // test parseExpirationFull with all valid separator - result, _ := ParseExpiration("2d1h3m47s") - correctValue := 176627 - if result != correctValue { - t.Fatal("Error in parseExpirationFull, want : ", correctValue, "got : ", result) - } -} - -func TestParseExpirationMissing(t *testing.T) { // test parseExpirationFull with all valid separator - result, _ := ParseExpiration("1h47s") - correctValue := 3647 - if result != correctValue { - t.Fatal("Error in ParseExpirationFull, want : ", correctValue, "got : ", result) - } -} - -func TestParseExpirationNull(t *testing.T) { // test ParseExpirationFull with all valid separator - result, _ := ParseExpiration("0") - correctValue := 0 - if result != correctValue { - t.Fatal("Error in ParseExpirationFull, want : ", correctValue, "got : ", result) - } -} - -func TestParseExpirationNegative(t *testing.T) { // test ParseExpirationFull with all valid separator - result, _ := ParseExpiration("-42h1m4s") - correctValue := -1 - if result != correctValue { - t.Fatal("Error in ParseExpirationFull, want : ", correctValue, "got : ", result) - } -} - -func TestParseExpirationInvalid(t *testing.T) { // test ParseExpirationFull with all valid separator - result, _ := ParseExpiration("8h42h1m1d4s") - correctValue := -1 - if result != correctValue { - t.Fatal("Error in ParseExpirationFull, want : ", correctValue, "got : ", result) - } -}