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:
40
.github/workflows/ci.yml
vendored
Normal file
40
.github/workflows/ci.yml
vendored
Normal 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
46
.github/workflows/release.yml
vendored
Normal 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
115
.goreleaser.yml
Normal 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
7
Dockerfile.goreleaser
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM alpine
|
||||
|
||||
EXPOSE 8000/tcp
|
||||
ENTRYPOINT ["pastebin"]
|
||||
|
||||
COPY pastebin /bin/pastebin
|
||||
COPY pb /bin/pb
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
127
server_test.go
127
server_test.go
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user