1
0
mirror of https://github.com/taigrr/pastebin synced 2026-04-12 04:51:27 -07:00

Merge pull request #2 from taigrr/cd/ci-and-tests

ci: add CI/CD workflows, GoReleaser config, and extended tests
This commit is contained in:
2026-03-09 01:53:01 -04:00
committed by GitHub
7 changed files with 382 additions and 0 deletions

40
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ["1.26"]
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Build
run: |
go build -v ./...
go build -v ./cmd/pb/
- name: Vet
run: go vet ./...
- name: Staticcheck
uses: dominikh/staticcheck-action@v1
with:
version: latest
- name: Test
run: go test -race -v ./...

46
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26"
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}

115
.goreleaser.yml Normal file
View File

@@ -0,0 +1,115 @@
version: 2
builds:
- id: pastebin
main: .
binary: pastebin
ldflags:
- -s -w -X main.version={{.Version}}
goos:
- linux
- darwin
- windows
- freebsd
goarch:
- amd64
- arm64
ignore:
- goos: windows
goarch: arm64
- goos: freebsd
goarch: arm64
- id: pb
main: ./cmd/pb
binary: pb
ldflags:
- -s -w -X main.version={{.Version}}
goos:
- linux
- darwin
- windows
- freebsd
goarch:
- amd64
- arm64
ignore:
- goos: windows
goarch: arm64
- goos: freebsd
goarch: arm64
archives:
- id: pastebin
builds:
- pastebin
- pb
format: tar.gz
format_overrides:
- goos: windows
format: zip
files:
- README.md
- LICENSE
name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
{{- .Os }}_
{{- .Arch }}
dockers:
- image_templates:
- "ghcr.io/taigrr/pastebin:{{ .Version }}-amd64"
- "ghcr.io/taigrr/pastebin:latest-amd64"
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
ids:
- pastebin
- pb
dockerfile: Dockerfile.goreleaser
- image_templates:
- "ghcr.io/taigrr/pastebin:{{ .Version }}-arm64"
- "ghcr.io/taigrr/pastebin:latest-arm64"
use: buildx
build_flag_templates:
- "--platform=linux/arm64"
ids:
- pastebin
- pb
goarch: arm64
dockerfile: Dockerfile.goreleaser
docker_manifests:
- name_template: "ghcr.io/taigrr/pastebin:{{ .Version }}"
image_templates:
- "ghcr.io/taigrr/pastebin:{{ .Version }}-amd64"
- "ghcr.io/taigrr/pastebin:{{ .Version }}-arm64"
- name_template: "ghcr.io/taigrr/pastebin:latest"
image_templates:
- "ghcr.io/taigrr/pastebin:latest-amd64"
- "ghcr.io/taigrr/pastebin:latest-arm64"
checksum:
name_template: "checksums.txt"
algorithm: sha256
changelog:
use: github
sort: asc
groups:
- title: Features
regexp: '^feat(\(.+\))?!?:'
- title: Bug Fixes
regexp: '^fix(\(.+\))?!?:'
- title: Documentation
regexp: '^docs(\(.+\))?!?:'
- title: Others
order: 999
release:
github:
owner: taigrr
name: pastebin

7
Dockerfile.goreleaser Normal file
View File

@@ -0,0 +1,7 @@
FROM alpine
EXPOSE 8000/tcp
ENTRYPOINT ["pastebin"]
COPY pastebin /bin/pastebin
COPY pb /bin/pb

View File

@@ -24,3 +24,36 @@ func TestConfig(t *testing.T) {
assert.Equal(t, "https://localhost", cfg.FQDN)
assert.Equal(t, "0.0.0.0:8000", cfg.Bind)
}
func TestConfigDefaults(t *testing.T) {
tests := []struct {
name string
config Config
expiry time.Duration
bind string
fqdn string
}{
{
name: "short expiry",
config: Config{Expiry: 1 * time.Minute, Bind: ":8080", FQDN: "paste.example.com"},
expiry: 1 * time.Minute,
bind: ":8080",
fqdn: "paste.example.com",
},
{
name: "long expiry",
config: Config{Expiry: 24 * time.Hour, Bind: "127.0.0.1:3000", FQDN: "localhost"},
expiry: 24 * time.Hour,
bind: "127.0.0.1:3000",
fqdn: "localhost",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expiry, tc.config.Expiry)
assert.Equal(t, tc.bind, tc.config.Bind)
assert.Equal(t, tc.fqdn, tc.config.FQDN)
})
}
}

View File

@@ -226,6 +226,133 @@ func TestPasteOversized(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rec.Code)
}
func TestViewHTMLRender(t *testing.T) {
server := newTestServer()
server.store.Set("htmlview", "rendered content", 0)
req := httptest.NewRequest(http.MethodGet, "/p/htmlview", nil)
req.Header.Set("Accept", "text/html")
rec := httptest.NewRecorder()
server.mux.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Header().Get("Content-Type"), "text/html")
assert.Contains(t, rec.Body.String(), "rendered content")
assert.Contains(t, rec.Body.String(), "htmlview")
}
func TestStatsEmptyStore(t *testing.T) {
server := newTestServer()
req := httptest.NewRequest(http.MethodGet, "/debug/stats", nil)
rec := httptest.NewRecorder()
server.mux.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"item_count":0`)
}
func TestStatsMultipleItems(t *testing.T) {
server := newTestServer()
server.store.Set("a", "1", 0)
server.store.Set("b", "2", 0)
server.store.Set("c", "3", 0)
req := httptest.NewRequest(http.MethodGet, "/debug/stats", nil)
rec := httptest.NewRecorder()
server.mux.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"item_count":3`)
}
func TestPasteNoFormField(t *testing.T) {
server := newTestServer()
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(""))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
server.mux.ServeHTTP(rec, req)
assert.Equal(t, http.StatusBadRequest, rec.Code)
}
func TestDeleteResponseBody(t *testing.T) {
server := newTestServer()
server.store.Set("delresp", "content", 0)
req := httptest.NewRequest(http.MethodDelete, "/p/delresp", nil)
rec := httptest.NewRecorder()
server.mux.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "Deleted", rec.Body.String())
}
func TestDownloadContentHeaders(t *testing.T) {
server := newTestServer()
server.store.Set("dlheader", "file content here", 0)
req := httptest.NewRequest(http.MethodGet, "/download/dlheader", nil)
rec := httptest.NewRecorder()
server.mux.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "attachment; filename=dlheader", rec.Header().Get("Content-Disposition"))
}
func TestNegotiateContentTypeDefault(t *testing.T) {
server := newTestServer()
// No Accept header defaults to plain text
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
server.mux.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "pastebin service")
}
func TestPasteRoundTripSpecialChars(t *testing.T) {
server := newTestServer()
specialContent := "line1\nline2\n<script>alert('xss')</script>\n日本語テスト"
formData := url.Values{}
formData.Set("blob", specialContent)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "text/plain")
rec := httptest.NewRecorder()
server.mux.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
parts := strings.Split(rec.Body.String(), "/p/")
require.Len(t, parts, 2)
pasteID := parts[1]
viewReq := httptest.NewRequest(http.MethodGet, "/p/"+pasteID, nil)
viewReq.Header.Set("Accept", "text/plain")
viewRec := httptest.NewRecorder()
server.mux.ServeHTTP(viewRec, viewReq)
assert.Equal(t, http.StatusOK, viewRec.Code)
assert.Equal(t, specialContent, viewRec.Body.String())
}
func TestViewWithTabs(t *testing.T) {
server := newTestServer()

View File

@@ -22,3 +22,17 @@ func TestRandomStringUniqueness(t *testing.T) {
seen[result] = true
}
}
func TestRandomStringURLSafe(t *testing.T) {
for range 50 {
result := RandomString(32)
// base64 URL encoding uses A-Z, a-z, 0-9, -, _
for _, char := range result {
isValid := (char >= 'A' && char <= 'Z') ||
(char >= 'a' && char <= 'z') ||
(char >= '0' && char <= '9') ||
char == '-' || char == '_' || char == '='
assert.True(t, isValid, "RandomString contains invalid URL character: %c", char)
}
}
}