Compare commits

...

6 Commits

Author SHA1 Message Date
Stefan Goppelt 6ca2c4d25f ci: improve drone test checks and coverage reporting
continuous-integration/drone/push Build is failing Details
2026-03-29 15:37:35 +02:00
Stefan Goppelt d01242ae7c docs(dod): enforce markdownlint rule for markdown files 2026-03-29 15:14:33 +02:00
Stefan Goppelt 6236717fdb Refactor documentation for clarity and consistency, update test function naming convention 2026-03-29 15:01:08 +02:00
Stefan Goppelt 2ec2fcd2ab Fix formatting of variable declarations in os_windows.go 2026-03-29 14:54:46 +02:00
Stefan Goppelt 8ace13074f Update CI pipeline to use Go 1.25.8, enhance Definition of Done with SonarQube checks, refactor SID allocation in Windows functions, and improve test coverage for JoiningSlash function. 2026-03-29 14:54:31 +02:00
Stefan Goppelt 1da053fb67 - apply latest AGENTS and DoD templates
- replace PowerShell reconcile script with shell-based reconcile workflow
- add .gitattributes, .editorconfig, and project-local git hooks
- configure .githooks as core.hooksPath
- refresh .gitignore entries from the shared standard
2026-03-29 14:15:29 +02:00
16 changed files with 156 additions and 284 deletions

View File

@ -4,9 +4,12 @@ name: go-lib/util
steps:
- name: test
image: golang:1.18
image: golang:1.25.8
commands:
- go get ./...
- go test ./...
- go vet ./...
- mkdir -p .build
- go test -v -coverprofile=.build/coverage.out ./...
- go tool cover -func=.build/coverage.out | tee .build/coverage.txt | awk '/^total:/ { gsub("%", "", $3); if ($3 + 0 < 80) { printf("Coverage %.1f%% is below 80%%\n", $3); exit 1 } }'
- go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck -v -json ./... > vulncheck.json
- govulncheck -json ./... > vulncheck.json

View File

@ -14,6 +14,3 @@ indent_size = 4
[*.md]
trim_trailing_whitespace = false
[*.ps1]
end_of_line = crlf

1
.gitattributes vendored
View File

@ -13,6 +13,5 @@
Makefile text eol=lf
# Keep native Windows script formats
*.ps1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf

View File

@ -14,3 +14,8 @@ The pre-commit hook validates for staged `.sh` files:
- executable bit in Git index (`100755`)
- LF line endings (no CRLF)
The pre-commit hook also validates staged `.md` files with `markdownlint`:
- no `markdownlint` errors or problems
- requires `markdownlint` CLI in PATH (for example via `npm install --global markdownlint-cli`)

View File

@ -5,6 +5,7 @@ failed=0
cr=$(printf '\r')
staged_shell_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.sh$' || true)
staged_markdown_files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.md$' || true)
for file in $staged_shell_files; do
mode=$(git ls-files --stage -- "$file" | awk '{print $1}')
@ -19,6 +20,20 @@ for file in $staged_shell_files; do
fi
done
if [ -n "$staged_markdown_files" ]; then
if ! command -v markdownlint >/dev/null 2>&1; then
echo "ERROR: markdownlint is required to validate staged Markdown files (.md)." >&2
echo "Install with npm: npm install --global markdownlint-cli" >&2
failed=1
else
# Validate the staged markdown files currently present in the working tree.
# This keeps the hook simple and fast for standard project usage.
if ! markdownlint $staged_markdown_files; then
failed=1
fi
fi
fi
if [ "$failed" -ne 0 ]; then
echo "Pre-commit check failed." >&2
exit 1

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ coverage.out
Thumbs.db
.vscode/
.idea/

View File

@ -1,14 +1,20 @@
# AGENTS.md
## Purpose
This file defines the default working agreement for AI coding agents and contributors in this repository.
This file defines the default working agreement for AI coding agents
and contributors in this repository.
## Documentation Language
Project language is English. All documentation, issues, pull requests, commit messages, and code comments should be written in English.
Project language is English. All documentation, issues, pull requests,
commit messages, and code comments should be written in English.
All newly created documentation in this project must be written in English.
All project documentation files must be stored under `docs/`, except `README.md` and `AGENTS.md`.
All project documentation files must be stored under `docs/`,
except `README.md` and `AGENTS.md`.
## Working Principles
1. Keep changes minimal and focused on the requested task.
2. Preserve existing public APIs unless a breaking change is explicitly requested.
3. Prefer clear, maintainable code over clever shortcuts.
@ -16,28 +22,46 @@ All project documentation files must be stored under `docs/`, except `README.md`
5. Never add secrets, credentials, or tokens to the repository.
## Testing Expectations
1. Add or update tests for behavior changes.
2. Keep tests deterministic and fast.
3. Prefer table-driven tests where they improve readability.
4. Run relevant tests locally before finishing changes.
5. For Go projects, use `github.com/smartystreets/goconvey` as the standard test library.
5. For Go projects, use `github.com/smartystreets/goconvey`
as the standard test library.
## Build Artifacts and Reports
1. Builder logs and generated reports must be created under `.build/`.
2. The `.build/` directory must be excluded from version control via `.gitignore`.
## Git and Script Standards
1. Shell scripts (`*.sh`) must use LF line endings.
2. Shell scripts committed to the repository must be executable in Git index (mode `100755`).
3. When adding a new shell script, set execute permissions before commit: `git add --chmod=+x path/to/script.sh`.
2. Shell scripts committed to the repository must be executable
in Git index (mode `100755`).
3. When adding a new shell script, set execute permissions
before commit: `git add --chmod=+x path/to/script.sh`.
## Git Bash Execution Defaults
1. Repository maintenance scripts are executed with Git Bash shell on Windows.
2. Default repository root is `~/git`.
3. The repository root can be overridden via `GO_GIT_ROOT`.
4. The standards repository defaults to `$ROOT_PATH/project-standards`.
5. The standards repository path can be overridden via `GO_PROJECT_STANDARDS`.
## Definition of Done (DoD)
### Purpose
The Definition of Done defines the minimum quality bar for every completed change in this repository.
### DoD Purpose
The Definition of Done defines the minimum quality bar
for every completed change in this repository.
### Mandatory Criteria
1. Tests
- Every code change is covered by tests where applicable.
- New functionality includes new tests.
- Bug fixes include at least one regression test.
@ -45,28 +69,41 @@ The Definition of Done defines the minimum quality bar for every completed chang
- Automated test coverage is at least 80%.
1. Functional documentation
- Implemented functionality is documented.
- Public API-relevant changes are reflected in README and/or docs.
1. Documentation standards
- Documentation is written in English.
- Documentation files are placed under `docs/`.
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
- Markdown files have no `markdownlint` errors or problems.
### Technical Completion Criteria
1. Build and test status
- The project builds successfully.
- Relevant test commands run successfully.
1. No unresolved critical issues
- No new blocking errors are introduced.
- Known non-blocking warnings are acceptable only if unrelated to the change or documented.
- Known non-blocking warnings are acceptable
only if unrelated to the change or documented.
1. SonarQube status
- No SonarQube errors are present.
1. Documentation structure
- Links to moved or newly added docs are valid.
- Documentation structure remains consistent with project rules.
### Review Checklist (Quick)
- [ ] Change is implemented and meets acceptance criteria.
- [ ] Tests were added/updated and pass.
- [ ] Go tests use `github.com/smartystreets/goconvey`.
@ -74,4 +111,6 @@ The Definition of Done defines the minimum quality bar for every completed chang
- [ ] Functionality is documented.
- [ ] Documentation is in English.
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
- [ ] Markdown files have no `markdownlint` errors or problems.
- [ ] No SonarQube errors are present.
- [ ] No critical regressions found.

View File

@ -1,7 +1,7 @@
<div style="text-align:left"><img src="https://www.yoorie.de/img/favicon_32.png"/></div>
# Go utility library
![yoorie.de logo](https://www.yoorie.de/img/favicon_32.png)
[![Build Status](https://drone.yoorie.de/api/badges/go-lib/util/status.svg)](https://drone.yoorie.de/go-lib/util)
## Project Description
@ -41,22 +41,20 @@ go get scm.yoorie.de/go-lib/util
package main
import (
"fmt"
"fmt"
"scm.yoorie.de/go-lib/util"
"scm.yoorie.de/go-lib/util"
)
func main() {
if util.FileExists("config.yaml") {
fmt.Println("config file found")
}
if util.FileExists("config.yaml") {
fmt.Println("config file found")
}
fmt.Println(util.JoiningSlash("/api", "v1", "users"))
fmt.Println(util.GetGlobalConfigurationFile("myapp", "config.yaml"))
fmt.Println(util.JoiningSlash("/api", "v1", "users"))
fmt.Println(util.GetGlobalConfigurationFile("myapp", "config.yaml"))
}
```
---
Copyright &copy; 2023 yoorie.de

View File

@ -1 +0,0 @@
mode: set

View File

@ -2,11 +2,13 @@
## Purpose
This Definition of Done defines the minimum quality bar for every completed change in this repository.
This Definition of Done defines the minimum quality bar
for every completed change in this repository.
## Mandatory Criteria
1. Tests
- Every code change is covered by tests where applicable.
- New functionality includes new tests.
- Bug fixes include at least one regression test.
@ -14,25 +16,36 @@ This Definition of Done defines the minimum quality bar for every completed chan
- Automated test coverage is at least 80%.
1. Functional documentation
- Implemented functionality is documented.
- Public API-relevant changes are reflected in README and/or docs.
1. Documentation standards
- Documentation is written in English.
- Documentation files are placed under `docs/`.
- Exceptions: `README.md` and `AGENTS.md` remain at repository root.
- Markdown files have no `markdownlint` errors or problems.
## Technical Completion Criteria
1. Build and test status
- The project builds successfully.
- Relevant test commands run successfully.
1. No unresolved critical issues
- No new blocking errors are introduced.
- Known non-blocking warnings are acceptable only if unrelated to the change or documented.
- Known non-blocking warnings are acceptable
only if unrelated to the change or documented.
1. SonarQube status
- No SonarQube errors are present.
1. Documentation links and structure
- Links to moved or newly added docs are valid.
- Documentation structure remains consistent with project rules.
@ -45,4 +58,6 @@ This Definition of Done defines the minimum quality bar for every completed chan
- [ ] Functionality is documented.
- [ ] Documentation is in English.
- [ ] Documentation is located under `docs/` (except `README.md` and `AGENTS.md`).
- [ ] Markdown files have no `markdownlint` errors or problems.
- [ ] No SonarQube errors are present.
- [ ] No critical regressions found.

View File

@ -13,9 +13,19 @@ import (
var (
allocateAndInitializeSid = windows.AllocateAndInitializeSid
freeSid = windows.FreeSid
tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) }
fatalf = log.Fatalf
allocateAdminGroupSid = func(sid **windows.SID) error {
return allocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY,
2,
windows.SECURITY_BUILTIN_DOMAIN_RID,
windows.DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
sid,
)
}
freeSid = windows.FreeSid
tokenIsMember = func(token windows.Token, sid *windows.SID) (bool, error) { return token.IsMember(sid) }
fatalf = log.Fatalf
)
// IsSuperUser returns true, if the current user is a super user
@ -27,13 +37,7 @@ func IsSuperUser() bool {
// official windows documentation. The Go API for this is a
// direct wrap around the official C++ API.
// See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
err := allocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY,
2,
windows.SECURITY_BUILTIN_DOMAIN_RID,
windows.DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&sid)
err := allocateAdminGroupSid(&sid)
if err != nil {
fatalf("SID Error: %s", err)
return false

View File

@ -11,33 +11,21 @@ import (
"golang.org/x/sys/windows"
)
func TestIsSuperUser_SidAllocationError(t *testing.T) {
func TestIsSuperUserSidAllocationError(t *testing.T) {
Convey("IsSuperUser should return false when SID allocation fails", t, func() {
origAllocate := allocateAndInitializeSid
origAllocate := allocateAdminGroupSid
origFatalf := fatalf
defer func() {
allocateAndInitializeSid = origAllocate
allocateAdminGroupSid = origAllocate
fatalf = origFatalf
}()
allocateAndInitializeSid = func(
authority *windows.SidIdentifierAuthority,
subAuthorityCount byte,
subAuthority0 uint32,
subAuthority1 uint32,
subAuthority2 uint32,
subAuthority3 uint32,
subAuthority4 uint32,
subAuthority5 uint32,
subAuthority6 uint32,
subAuthority7 uint32,
sid **windows.SID,
) error {
allocateAdminGroupSid = func(_ **windows.SID) error {
return errors.New("forced sid allocation error")
}
fatalCalled := false
fatalf = func(format string, v ...interface{}) {
fatalf = func(_ string, _ ...interface{}) {
fatalCalled = true
}

View File

@ -1,217 +0,0 @@
[CmdletBinding()]
param(
[string]$RootPath = "C:\Users\stefan\git",
[string]$StandardsRepoPath = "C:\Users\stefan\git\project-standards",
[string[]]$Exclude = @('project-standards'),
[switch]$CheckOnly,
[switch]$Watch,
[int]$IntervalSeconds = 60
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$agentsTemplate = Join-Path $StandardsRepoPath 'templates/AGENTS.base.md'
$dodTemplate = Join-Path $StandardsRepoPath 'templates/DEFINITION_OF_DONE.base.md'
$gitIgnoreTemplate = Join-Path $StandardsRepoPath 'templates/.gitignore.base'
$gitAttributesTemplate = Join-Path $StandardsRepoPath 'templates/.gitattributes.base'
$editorConfigTemplate = Join-Path $StandardsRepoPath 'templates/.editorconfig.base'
$preCommitHookTemplate = Join-Path $StandardsRepoPath 'templates/pre-commit.base.sh'
$hooksReadmeTemplate = Join-Path $StandardsRepoPath 'templates/.githooks.README.base.md'
if (-not (Test-Path -Path $agentsTemplate -PathType Leaf)) {
throw "AGENTS template not found: $agentsTemplate"
}
if (-not (Test-Path -Path $dodTemplate -PathType Leaf)) {
throw "DoD template not found: $dodTemplate"
}
if (-not (Test-Path -Path $gitIgnoreTemplate -PathType Leaf)) {
throw "gitignore template not found: $gitIgnoreTemplate"
}
if (-not (Test-Path -Path $gitAttributesTemplate -PathType Leaf)) {
throw "gitattributes template not found: $gitAttributesTemplate"
}
if (-not (Test-Path -Path $editorConfigTemplate -PathType Leaf)) {
throw "editorconfig template not found: $editorConfigTemplate"
}
if (-not (Test-Path -Path $preCommitHookTemplate -PathType Leaf)) {
throw "pre-commit hook template not found: $preCommitHookTemplate"
}
if (-not (Test-Path -Path $hooksReadmeTemplate -PathType Leaf)) {
throw "hooks readme template not found: $hooksReadmeTemplate"
}
if ($IntervalSeconds -lt 5) {
throw 'IntervalSeconds must be >= 5.'
}
function Get-ContentHashOrMissing {
param([string]$Path)
if (-not (Test-Path -Path $Path -PathType Leaf)) {
return '__MISSING__'
}
return (Get-FileHash -Path $Path -Algorithm SHA256).Hash
}
function Ensure-FileFromTemplate {
param(
[string]$Template,
[string]$Target,
[switch]$OnlyCheck
)
$templateHash = Get-ContentHashOrMissing -Path $Template
$targetHash = Get-ContentHashOrMissing -Path $Target
if ($templateHash -eq $targetHash) {
return 'ok'
}
if ($OnlyCheck) {
return 'drift'
}
$parent = Split-Path -Parent $Target
if (-not (Test-Path -Path $parent -PathType Container)) {
New-Item -ItemType Directory -Path $parent | Out-Null
}
Copy-Item -Path $Template -Destination $Target -Force
return 'updated'
}
function Ensure-GitIgnoreEntriesFromTemplate {
param(
[string]$TemplatePath,
[string]$GitIgnorePath,
[switch]$OnlyCheck
)
$requiredEntries = Get-Content -Path $TemplatePath | Where-Object {
$_.Trim() -and -not $_.Trim().StartsWith('#')
}
if (-not (Test-Path -Path $GitIgnorePath -PathType Leaf)) {
if ($OnlyCheck) {
return 'drift'
}
New-Item -ItemType File -Path $GitIgnorePath | Out-Null
}
$lines = Get-Content -Path $GitIgnorePath
$missingEntry = $false
foreach ($entry in $requiredEntries) {
if ($lines -contains $entry) {
continue
}
$missingEntry = $true
if (-not $OnlyCheck) {
Add-Content -Path $GitIgnorePath -Value $entry
$lines += $entry
}
}
if (-not $missingEntry) {
return 'ok'
}
if ($OnlyCheck) {
return 'drift'
}
return 'updated'
}
function Invoke-ReconcileOnce {
param([switch]$OnlyCheck)
$resolvedRoot = (Resolve-Path -Path $RootPath -ErrorAction Stop).Path
$repos = Get-ChildItem -Path $resolvedRoot -Directory | Where-Object {
$Exclude -notcontains $_.Name
}
$summary = [ordered]@{
scanned = 0
updated = 0
drift = 0
}
foreach ($repo in $repos) {
$repoPath = $repo.FullName
$agentsTarget = Join-Path $repoPath 'AGENTS.md'
$dodTarget = Join-Path (Join-Path $repoPath 'docs') 'DEFINITION_OF_DONE.md'
$gitIgnoreTarget = Join-Path $repoPath '.gitignore'
$gitAttributesTarget = Join-Path $repoPath '.gitattributes'
$editorConfigTarget = Join-Path $repoPath '.editorconfig'
$preCommitHookTarget = Join-Path (Join-Path $repoPath '.githooks') 'pre-commit'
$hooksReadmeTarget = Join-Path (Join-Path $repoPath '.githooks') 'README.md'
$summary.scanned++
$agentsState = Ensure-FileFromTemplate -Template $agentsTemplate -Target $agentsTarget -OnlyCheck:$OnlyCheck
$dodState = Ensure-FileFromTemplate -Template $dodTemplate -Target $dodTarget -OnlyCheck:$OnlyCheck
$gitAttributesState = Ensure-FileFromTemplate -Template $gitAttributesTemplate -Target $gitAttributesTarget -OnlyCheck:$OnlyCheck
$editorConfigState = Ensure-FileFromTemplate -Template $editorConfigTemplate -Target $editorConfigTarget -OnlyCheck:$OnlyCheck
$preCommitHookState = Ensure-FileFromTemplate -Template $preCommitHookTemplate -Target $preCommitHookTarget -OnlyCheck:$OnlyCheck
$hooksReadmeState = Ensure-FileFromTemplate -Template $hooksReadmeTemplate -Target $hooksReadmeTarget -OnlyCheck:$OnlyCheck
$gitIgnoreState = Ensure-GitIgnoreEntriesFromTemplate -TemplatePath $gitIgnoreTemplate -GitIgnorePath $gitIgnoreTarget -OnlyCheck:$OnlyCheck
if (
$agentsState -eq 'updated' -or
$dodState -eq 'updated' -or
$gitAttributesState -eq 'updated' -or
$editorConfigState -eq 'updated' -or
$preCommitHookState -eq 'updated' -or
$hooksReadmeState -eq 'updated' -or
$gitIgnoreState -eq 'updated'
) {
$summary.updated++
Write-Host "UPDATED: $repoPath"
continue
}
if (
$agentsState -eq 'drift' -or
$dodState -eq 'drift' -or
$gitAttributesState -eq 'drift' -or
$editorConfigState -eq 'drift' -or
$preCommitHookState -eq 'drift' -or
$hooksReadmeState -eq 'drift' -or
$gitIgnoreState -eq 'drift'
) {
$summary.drift++
Write-Host "DRIFT: $repoPath"
continue
}
Write-Host "OK: $repoPath"
}
Write-Host "Summary -> scanned=$($summary.scanned), updated=$($summary.updated), drift=$($summary.drift)"
return $summary
}
if ($Watch) {
while ($true) {
$now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Write-Host "[$now] Reconciling project standards..."
[void](Invoke-ReconcileOnce -OnlyCheck:$CheckOnly)
Start-Sleep -Seconds $IntervalSeconds
}
}
$result = Invoke-ReconcileOnce -OnlyCheck:$CheckOnly
if ($CheckOnly -and $result.drift -gt 0) {
exit 1
}

View File

@ -1,8 +1,9 @@
#!/usr/bin/env sh
set -eu
ROOT_PATH="/c/Users/stefan/git"
STANDARDS_REPO="/c/Users/stefan/git/project-standards"
ROOT_PATH="${GO_GIT_ROOT:-$HOME/git}"
STANDARDS_REPO=""
STANDARDS_REPO_SET=0
EXCLUDE_NAME="project-standards"
CHECK_ONLY=0
WATCH=0
@ -21,6 +22,14 @@ Options:
--watch Continuously scan and reconcile
--interval <seconds> Watch interval in seconds (default: 60)
-h, --help Show this help
Environment:
GO_GIT_ROOT Overrides default root path (default: ~/git)
GO_PROJECT_STANDARDS Overrides standards repository path
Default resolution:
ROOT_PATH defaults to GO_GIT_ROOT or ~/git.
STANDARDS_REPO defaults to GO_PROJECT_STANDARDS or $ROOT_PATH/project-standards.
EOF
}
@ -32,6 +41,7 @@ while [ "$#" -gt 0 ]; do
;;
--standards-repo)
STANDARDS_REPO=$2
STANDARDS_REPO_SET=1
shift 2
;;
--exclude)
@ -62,6 +72,14 @@ while [ "$#" -gt 0 ]; do
esac
done
if [ "$STANDARDS_REPO_SET" -ne 1 ]; then
if [ -n "${GO_PROJECT_STANDARDS:-}" ]; then
STANDARDS_REPO="$GO_PROJECT_STANDARDS"
else
STANDARDS_REPO="$ROOT_PATH/project-standards"
fi
fi
if [ "$INTERVAL" -lt 5 ]; then
echo "interval must be >= 5" >&2
exit 1
@ -162,6 +180,7 @@ ensure_gitignore_entries_from_template() {
fi
while IFS= read -r entry; do
entry=$(printf '%s' "$entry" | tr -d '\r')
[ -n "$entry" ] || continue
case "$entry" in
\#*)
@ -169,7 +188,7 @@ ensure_gitignore_entries_from_template() {
;;
esac
if grep -Fqx "$entry" "$gitignore_path"; then
if tr -d '\r' < "$gitignore_path" | grep -Fqx "$entry"; then
continue
fi

View File

@ -34,7 +34,6 @@ func joiningSlash(elem []string) string {
}
func singleJoiningSlash(a, b string) string {
filepath.Join(a, b)
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {

View File

@ -23,12 +23,20 @@ func TestFileExists(t *testing.T) {
func TestJoiningSlash(t *testing.T) {
Convey("JoiningSlash should combine URL-like segments safely", t, func() {
So(JoiningSlash("http://my.tld/docs/", "bla/", "blub/"), ShouldEqual, "http://my.tld/docs/bla/blub/")
So(JoiningSlash("http://my.tld", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub")
So(JoiningSlash("http://my.tld/", "bla", "blub"), ShouldEqual, "http://my.tld/bla/blub")
So(JoiningSlash("http://my.tld", "bla/", "blub"), ShouldEqual, "http://my.tld/bla/blub")
So(JoiningSlash("http://my.tld/docs", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub")
So(JoiningSlash("http://my.tld/docs/", "bla/", "blub"), ShouldEqual, "http://my.tld/docs/bla/blub")
const (
baseURL = "http://my.tld"
docsURL = "http://my.tld/docs"
expectedRoot = "http://my.tld/bla/blub"
expectedDocs = "http://my.tld/docs/bla/blub"
expectedDocsS = "http://my.tld/docs/bla/blub/"
)
So(JoiningSlash(docsURL+"/", "bla/", "blub/"), ShouldEqual, expectedDocsS)
So(JoiningSlash(baseURL, "bla", "blub"), ShouldEqual, expectedRoot)
So(JoiningSlash(baseURL+"/", "bla", "blub"), ShouldEqual, expectedRoot)
So(JoiningSlash(baseURL, "bla/", "blub"), ShouldEqual, expectedRoot)
So(JoiningSlash(docsURL, "bla/", "blub"), ShouldEqual, expectedDocs)
So(JoiningSlash(docsURL+"/", "bla/", "blub"), ShouldEqual, expectedDocs)
So(JoiningSlash("", "api", "v1"), ShouldEqual, "api/v1")
So(JoiningSlash("", "", ""), ShouldEqual, "")
})