13 Commits

Author SHA1 Message Date
7c236a49b6 Add GitHub Actions build workflow and update Docker images
All checks were successful
🏗️✨ Build Workflow / 🖥️ 🔨 Build (push) Successful in 11m12s
2025-08-08 14:43:19 -04:00
df992caf1e Add Docker Compose configuration for Traefik and Redis services 2025-08-08 14:40:19 -04:00
88ef0c23a0 Implement Redis caching for Reddit image fetching 2025-07-22 12:13:38 -04:00
8d0f11e681 [no message] 2025-04-24 20:52:11 -04:00
c86eb6520d LOL! 2025-04-22 13:56:35 -04:00
a05558351f hmmm 2025-04-21 23:05:42 -04:00
4ff0c534c6 hoo boy 2025-04-21 23:04:34 -04:00
92d4be2f23 ALL SORTS OF THINGS 2025-04-21 23:04:24 -04:00
b79b34e8cc Merge branch 'code-reorg' into 'master'
Push poorly written test can down the road another ten years

See merge request rrise/top-wallpaper!2
2025-04-21 22:43:34 -04:00
c00032c7c4 Push poorly written test can down the road another ten years 2025-04-21 22:42:38 -04:00
9585e300fb Merge branch 'rrise-master-patch-31953' into 'master'
Add new file

See merge request rrise/top-wallpaper!1
2025-04-21 22:31:47 -04:00
280af327ed Add new file 2025-04-21 22:31:04 -04:00
c49e6d6c54 betterer code 2025-04-21 21:57:54 -04:00
14 changed files with 629 additions and 223 deletions

View File

@@ -0,0 +1,43 @@
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to build and push'
required: false
default: 'latest'
push:
tags:
- "v*"
name: 🏗️✨ Build Workflow
jobs:
Build:
name: 🖥️ 🔨 Build
runs-on: ubuntu-latest
steps:
- name: 📖 🔍 Checkout Repository Code
uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: 'true'
- name: 🔑 🔐 Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: 🏗️ 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🐳 🔨 Build Container
uses: docker/build-push-action@v6
with:
push: true
sbom: true
provenance: true
platforms: linux/arm64,linux/amd64
context: .
dockerfile: Dockerfile
tags: siteworxpro/top-wallpaper:${{ gitea.ref_name }}

View File

@@ -1,14 +1,12 @@
stages: stages:
- noop - Testing
- build - build
noop:
stage: noop
script:
- echo "This is a no-op job"
include: include:
- component: $CI_SERVER_FQDN/shared/blueprints/docker-build@v1.1.1 - component: $CI_SERVER_FQDN/shared/blueprints/golang-tests@v1.2.0
rules:
- if: '$CI_COMMIT_BRANCH'
- component: $CI_SERVER_FQDN/shared/blueprints/docker-build@v1.2.0
rules: rules:
- if: '$CI_COMMIT_TAG' - if: '$CI_COMMIT_TAG'
inputs: inputs:

View File

@@ -1,4 +1,4 @@
FROM siteworxpro/golang:1.24.0 AS build FROM siteworxpro/golang:1.24.3 AS build
WORKDIR /app WORKDIR /app

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Siteworx Professionals, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

50
docker-compose.yml Normal file
View File

@@ -0,0 +1,50 @@
volumes:
redis_data:
services:
traefik:
image: traefik:latest
container_name: traefik
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
restart: always
command:
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web-secure.address=:443"
- "--accesslog=true"
- "--entrypoints.web.http.redirections.entryPoint.to=web-secure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
redis:
image: redis:latest
container_name: redis
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
volumes:
- redis_data:/data
api:
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.entrypoints=web-secure"
- "traefik.http.routers.api.rule=Host(`localhost`) || Host(`127.0.0.1`)"
- "traefik.http.routers.api.tls=true"
- "traefik.http.services.api.loadbalancer.server.port=8080"
image: siteworxpro/top-wallpaper:v1.3.1
container_name: top-wallpaper
environment:
REDIS_URL: "redis"
CORS_ORIGINS: "https://localhost"
depends_on:
redis:
condition: service_healthy

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/siteworxpro/top-wallpaper module github.com/siteworxpro/top-wallpaper
go 1.24.0 go 1.24.3
require ( require (
github.com/labstack/echo/v4 v4.13.3 github.com/labstack/echo/v4 v4.13.3

25
http/handler.go Normal file
View File

@@ -0,0 +1,25 @@
package http
import (
"github.com/labstack/echo/v4"
"github.com/siteworxpro/top-wallpaper/redis"
"net/http"
)
func Get(c echo.Context) error {
rc, err := redis.NewRedis()
if err != nil {
return c.String(http.StatusInternalServerError, "Internal Server Error: "+err.Error())
}
val, err := rc.Get(redis.CacheKey)
if err != nil || val == "" {
return c.NoContent(http.StatusNoContent)
}
c.Response().Header().Set("Cache-Control", "public, max-age=600")
return c.Blob(http.StatusOK, "image/jpeg", []byte(val))
}

236
main.go
View File

@@ -2,123 +2,36 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"github.com/redis/go-redis/v9" tphttp "github.com/siteworxpro/top-wallpaper/http"
"github.com/siteworxpro/top-wallpaper/resize" "github.com/siteworxpro/top-wallpaper/reddit"
"io" "github.com/siteworxpro/top-wallpaper/redis"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"time"
) )
type redditResponse struct {
Kind string `json:"kind"`
Data struct {
After string `json:"after"`
Dist int `json:"dist"`
ModHash string `json:"modhash"`
GeoFilter string `json:"geo_filter"`
Children []struct {
Kind string `json:"kind"`
Data struct {
ApprovedAtUtc interface{} `json:"approved_at_utc"`
Subreddit string `json:"subreddit"`
SelfText string `json:"selftext"`
Url string `json:"url"`
UrlOverriddenByDest string `json:"url_overridden_by_dest"`
MediaMetadata map[string]struct {
Status string `json:"status"`
Id string `json:"id"`
E string `json:"e"`
M string `json:"m"`
S struct {
U string `json:"u"`
X int `json:"x"`
Y int `json:"y"`
} `json:"s"`
P []struct {
U string `json:"u"`
X int `json:"x"`
Y int `json:"y"`
} `json:"p"`
} `json:"media_metadata"`
Preview struct {
Enabled bool `json:"enabled"`
Images []struct {
Source struct {
Url string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
} `json:"source"`
} `json:"images"`
} `json:"preview"`
} `json:"data"`
} `json:"children"`
} `json:"data"`
}
type CustomContext struct {
echo.Context
redis *redis.Client
}
func main() { func main() {
ctx, fn := context.WithCancel(context.Background())
defer fn()
rc, err := redis.NewRedis()
if err != nil {
log.Error("Could not initialize Redis client: ", err)
os.Exit(1)
}
e := echo.New() e := echo.New()
e.AcquireContext().Set("redisClient", rc)
e.Logger.SetOutput(os.Stdout)
e.Logger.SetLevel(log.INFO) e.Logger.SetLevel(log.INFO)
e.HideBanner = true e.HideBanner = true
// Middleware
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
redisUrl := os.Getenv("REDIS_URL")
if redisUrl == "" {
c.Logger().Warn("REDIS_URL not set, skipping redis connection. Use REDIS_URL to cache the image")
return next(&CustomContext{c, nil})
}
redisPort := os.Getenv("REDIS_PORT")
if redisPort == "" {
redisPort = "6379"
}
redisDb := os.Getenv("REDIS_DB")
if redisDb == "" {
redisDb = "0"
}
redisDbInt, err := strconv.ParseInt(redisDb, 10, 64)
if err != nil {
c.Logger().Warn("REDIS_DB is not a valid integer, skipping redis connection. Use REDIS_DB to cache the image")
return next(&CustomContext{})
}
cc := &CustomContext{c, redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", redisUrl, redisPort),
Password: os.Getenv("REDIS_PASSWORD"),
DB: int(redisDbInt),
})}
cmd := cc.redis.Ping(context.Background())
if cmd.Err() != nil {
c.Logger().Warn("could not connect to redis")
cc.redis = nil
}
return next(cc)
}
})
e.Use(middleware.Logger()) e.Use(middleware.Logger())
e.Use(middleware.RequestID()) e.Use(middleware.RequestID())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
@@ -131,125 +44,20 @@ func main() {
if path == "" { if path == "" {
path = "/" path = "/"
} }
e.GET(path, func(c echo.Context) error {
cc := c.(*CustomContext)
var latestImageVal string e.GET(path, tphttp.Get)
var err error
if cc.redis != nil {
latestImage := cc.redis.Get(context.TODO(), "latestImage")
latestImageVal, err = latestImage.Result()
}
if err != nil || latestImageVal == "" {
c.Logger().Info("Fetching latest image")
latestImageVal, err = getLatestImage()
if err != nil {
return c.String(http.StatusInternalServerError, "Error fetching latest image")
}
if cc.redis != nil {
cmd := cc.redis.Set(context.TODO(), "latestImage", latestImageVal, 600*time.Second)
if cmd.Err() != nil {
c.Logger().Warn("could not cache image")
}
}
} else {
c.Logger().Info("Image name fetched from cache")
}
var imageData string
if cc.redis != nil {
latestImageBin := cc.redis.Get(context.TODO(), "latestImage:bin:"+latestImageVal)
imageData = latestImageBin.Val()
}
if imageData == "" {
response, err := http.Get(latestImageVal)
if err != nil {
return c.String(http.StatusInternalServerError, "Error fetching image")
}
if response.StatusCode != http.StatusOK {
return c.String(http.StatusInternalServerError, "Error fetching image")
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(response.Body)
imageDataBytes, err := io.ReadAll(response.Body)
if err != nil {
return c.String(http.StatusInternalServerError, "Error fetching image")
}
imageData = string(imageDataBytes)
imageData, err := resize.Shrink(imageData, 1200, 70)
if err != nil {
return c.String(http.StatusInternalServerError, "Error resizing image")
}
go func(data string) {
if cc.redis == nil {
return
}
_, err = cc.redis.Set(context.TODO(), "latestImage:bin:"+latestImageVal, data, 600*time.Second).Result()
if err != nil {
c.Logger().Warn("could not cache image")
}
}(imageData)
} else {
c.Logger().Info("Image data fetched from cache")
}
c.Response().Header().Set("Cache-Control", "public, max-age=600")
return c.Blob(http.StatusOK, "image/jpeg", []byte(imageData))
})
e.Logger.Info("Starting server at path " + path) e.Logger.Info("Starting server at path " + path)
// start the server
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "8080" port = "8080"
} }
ctx = redis.WithContext(ctx, rc)
ctx = context.WithValue(ctx, "logger", e.Logger)
go reddit.Fetch(ctx)
e.Logger.Fatal(e.Start(":" + port)) e.Logger.Fatal(e.Start(":" + port))
} }
func getLatestImage() (string, error) {
response, err := http.Get("https://www.reddit.com/r/wallpaper/.json")
if err != nil {
return "", err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(response.Body)
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("error fetching reddit data: %s", response.Status)
}
jsonBytes, err := io.ReadAll(response.Body)
redditData := redditResponse{}
err = json.Unmarshal(jsonBytes, &redditData)
if err != nil {
return "", err
}
index := 1
url := redditData.Data.Children[index].Data.UrlOverriddenByDest
for strings.Contains(url, "gallery") {
index++
url = redditData.Data.Children[index].Data.UrlOverriddenByDest
}
return url, nil
}

88
reddit/client.go Normal file
View File

@@ -0,0 +1,88 @@
package reddit
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
type redditResponse struct {
Kind string `json:"kind"`
Data struct {
After string `json:"after"`
Dist int `json:"dist"`
ModHash string `json:"modhash"`
GeoFilter string `json:"geo_filter"`
Children []struct {
Kind string `json:"kind"`
Data struct {
ApprovedAtUtc interface{} `json:"approved_at_utc"`
Subreddit string `json:"subreddit"`
SelfText string `json:"selftext"`
Url string `json:"url"`
UrlOverriddenByDest string `json:"url_overridden_by_dest"`
MediaMetadata map[string]struct {
Status string `json:"status"`
Id string `json:"id"`
E string `json:"e"`
M string `json:"m"`
S struct {
U string `json:"u"`
X int `json:"x"`
Y int `json:"y"`
} `json:"s"`
P []struct {
U string `json:"u"`
X int `json:"x"`
Y int `json:"y"`
} `json:"p"`
} `json:"media_metadata"`
Preview struct {
Enabled bool `json:"enabled"`
Images []struct {
Source struct {
Url string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
} `json:"source"`
} `json:"images"`
} `json:"preview"`
} `json:"data"`
} `json:"children"`
} `json:"data"`
}
func GetLatestImage(httpGet func(url string) (*http.Response, error)) (string, error) {
response, err := httpGet("https://www.reddit.com/r/wallpaper/.json")
if err != nil {
return "", err
}
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(response.Body)
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("error fetching reddit data: %s", response.Status)
}
jsonBytes, err := io.ReadAll(response.Body)
redditData := redditResponse{}
err = json.Unmarshal(jsonBytes, &redditData)
if err != nil {
return "", err
}
index := 1
url := redditData.Data.Children[index].Data.UrlOverriddenByDest
for strings.Contains(url, "gallery") {
index++
url = redditData.Data.Children[index].Data.UrlOverriddenByDest
}
return url, nil
}

70
reddit/client_test.go Normal file
View File

@@ -0,0 +1,70 @@
package reddit
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestGetLatestImage(t *testing.T) {
// Mock Reddit API response
mockResponse := `{
"kind": "Listing",
"data": {
"after": null,
"dist": 1,
"modhash": "",
"geo_filter": "",
"children": [
{
"kind": "t3",
"data": {
"url_overridden_by_dest": "https://example.com/gallery",
"media_metadata": {},
"preview": {}
}
},
{
"kind": "t3",
"data": {
"url_overridden_by_dest": "https://example.com/image.jpg",
"media_metadata": {},
"preview": {}
}
}
]
}
}`
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/r/wallpaper/.json") {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(mockResponse))
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// Custom httpGet function for testing
httpGet := func(url string) (*http.Response, error) {
if strings.Contains(url, "/r/wallpaper/.json") {
return http.Get(server.URL + "/r/wallpaper/.json")
}
return nil, fmt.Errorf("unexpected URL: %s", url)
}
// Test the function
imageURL, err := GetLatestImage(httpGet)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
expectedURL := "https://example.com/image.jpg"
if imageURL != expectedURL {
t.Errorf("expected %s, got %s", expectedURL, imageURL)
}
}

132
reddit/fetcher.go Normal file
View File

@@ -0,0 +1,132 @@
package reddit
import (
"context"
"github.com/labstack/echo/v4"
"github.com/siteworxpro/top-wallpaper/redis"
"github.com/siteworxpro/top-wallpaper/resize"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func Fetch(ctx context.Context) {
const defaultImageSize = 1600
const minImageSize = 100
rc, err := redis.FromContext(ctx)
if err != nil {
panic("Redis client not found in context: " + err.Error())
}
l := ctx.Value("logger").(echo.Logger)
var size int
if sizeS, ok := os.LookupEnv("MAX_SIZE"); ok {
size, err = strconv.Atoi(sizeS)
if err != nil {
size = defaultImageSize
}
} else {
size = defaultImageSize
}
if size < minImageSize {
size = defaultImageSize
}
for {
select {
case <-ctx.Done():
l.Info("Stopping Reddit fetcher...")
_ = rc.Close()
return
default:
val, err := rc.Get(redis.CacheKey)
if val != "" {
l.Info("Reddit image fetched from cache...")
break
}
l.Info("Fetching latest image from Reddit...")
latestImageVal, err := GetLatestImage(func(u string) (*http.Response, error) {
request := &http.Request{
Method: http.MethodGet,
Header: http.Header{
"User-Agent": []string{"Mozilla/5.0 (compatible; RedditBot/1.0; +https://www.reddit.com/wiki/redditauth)"},
},
URL: &url.URL{
Scheme: "https",
Host: "www.reddit.com",
Path: strings.TrimPrefix(u, "https://www.reddit.com"),
},
}
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
response, err := httpClient.Do(request)
if err != nil {
l.Error("Error fetching image from Reddit: ", err)
return nil, err
}
if response.StatusCode != http.StatusOK {
l.Error("Error fetching image from Reddit: ", response.Status)
return nil, &echo.HTTPError{
Code: response.StatusCode,
Message: "Error fetching image from Reddit",
}
}
return response, nil
})
response, err := http.Get(latestImageVal)
if err != nil {
l.Error("Error fetching image from Reddit: ", err)
break
}
if response.StatusCode != http.StatusOK {
l.Error("Error fetching image from Reddit: ", response.Status)
break
}
imageDataBytes, err := io.ReadAll(response.Body)
if err != nil {
l.Error("Error reading image data: ", err)
_ = response.Body.Close()
break
}
_ = response.Body.Close()
imageData := string(imageDataBytes)
resized, err := resize.Shrink(imageData, uint(size), 70)
err = rc.Set(redis.CacheKey, resized, 10*time.Minute)
if err != nil {
l.Warn("could not cache image")
}
l.Info("Reddit image fetched and resized successfully, cached for 10 minutes.")
}
time.Sleep(10 * time.Minute)
}
}

118
redis/client.go Normal file
View File

@@ -0,0 +1,118 @@
package redis
import (
"context"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"os"
"strconv"
"time"
)
type contextKey string
const redisKey contextKey = "redisClient"
const CacheKey = "top-wallpaper:latestImage"
type Cache interface {
Get(key string) (string, error)
Set(key string, value string, expiration int64) error
Close() error
}
type Redis struct {
client *redis.Client
}
func NewRedis() (*Redis, error) {
redisUrl := os.Getenv("REDIS_URL")
if redisUrl == "" {
return nil, errors.New("REDIS_URL not set, cannot initialize Redis client")
}
redisPort := os.Getenv("REDIS_PORT")
if redisPort == "" {
redisPort = "6379"
}
redisDb := os.Getenv("REDIS_DB")
if redisDb == "" {
redisDb = "0"
}
redisDbInt, err := strconv.ParseInt(redisDb, 10, 64)
if err != nil {
return nil, errors.New("REDIS_DB is not a valid integer, cannot initialize Redis client")
}
rc := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", redisUrl, redisPort),
Password: os.Getenv("REDIS_PASSWORD"),
DB: int(redisDbInt),
})
cmd := rc.Ping(context.Background())
if cmd.Err() != nil {
return nil, fmt.Errorf("could not connect to Redis: %v", cmd.Err())
}
if cmd.Val() != "PONG" {
return nil, fmt.Errorf("unexpected response from Redis: %s", cmd.Val())
}
return &Redis{
client: rc,
}, nil
}
func (r *Redis) Get(key string) (string, error) {
cmd := r.client.Get(context.Background(), key)
if cmd.Err() != nil {
if errors.Is(cmd.Err(), redis.Nil) {
return "", nil // Key does not exist
}
return "", fmt.Errorf("could not get value for key %s: %v", key, cmd.Err())
}
return cmd.Val(), nil
}
func (r *Redis) Set(key string, value string, expiration time.Duration) error {
cmd := r.client.Set(context.Background(), key, value, expiration)
if cmd.Err() != nil {
return fmt.Errorf("could not set value for key %s: %v", key, cmd.Err())
}
if cmd.Val() != "OK" {
return fmt.Errorf("unexpected response from Redis when setting key %s: %s", key, cmd.Val())
}
return nil
}
func (r *Redis) Close() error {
if r.client == nil {
return nil
}
err := r.client.Close()
if err != nil {
return fmt.Errorf("could not close Redis client: %v", err)
}
return nil
}
func WithContext(ctx context.Context, r *Redis) context.Context {
return context.WithValue(ctx, redisKey, r)
}
func FromContext(ctx context.Context) (*Redis, error) {
r, ok := ctx.Value(redisKey).(*Redis)
if !ok {
return nil, errors.New("no Redis client found in context")
}
if r == nil {
return nil, errors.New("redis client is nil")
}
return r, nil
}

View File

@@ -2,6 +2,7 @@ package resize
import ( import (
"bytes" "bytes"
"fmt"
"github.com/nfnt/resize" "github.com/nfnt/resize"
i "image" i "image"
"image/jpeg" "image/jpeg"
@@ -9,6 +10,10 @@ import (
) )
func Shrink(image string, maxSize uint, quality int) (string, error) { func Shrink(image string, maxSize uint, quality int) (string, error) {
if quality < 1 || quality > 100 {
return "", fmt.Errorf("quality must be between 1 and 100, got %d", quality)
}
img, _, err := i.Decode(bytes.NewReader([]byte(image))) img, _, err := i.Decode(bytes.NewReader([]byte(image)))
if err != nil { if err != nil {
return "", err return "", err

48
resize/resize_test.go Normal file
View File

@@ -0,0 +1,48 @@
package resize
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"testing"
)
func createTestImage() []byte {
img := image.NewRGBA(image.Rect(0, 0, 100, 100))
for x := 0; x < 100; x++ {
for y := 0; y < 100; y++ {
img.Set(x, y, color.RGBA{uint8(x), uint8(y), 255, 255})
}
}
var buf bytes.Buffer
_ = jpeg.Encode(&buf, img, nil)
return buf.Bytes()
}
func TestShrink_Success(t *testing.T) {
testImage := createTestImage()
result, err := Shrink(string(testImage), 50, 80)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if len(result) == 0 {
t.Fatalf("expected non-empty result, got empty string")
}
}
func TestShrink_InvalidImage(t *testing.T) {
invalidImage := "not-an-image"
_, err := Shrink(invalidImage, 50, 80)
if err == nil {
t.Fatalf("expected an error for invalid image input, got nil")
}
}
func TestShrink_InvalidQuality(t *testing.T) {
testImage := createTestImage()
_, err := Shrink(string(testImage), 50, -10)
if err == nil {
t.Fatalf("expected an error for invalid quality, got nil")
}
}