Files
privatae-bastion/client-go/stepup_qr_test.go
T
Privatae e4c9b69b46
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s
bastion 0.53.2
2026-10-20 04:42:14 +00:00

137 lines
4.1 KiB
Go

package main
import (
"os"
"strings"
"testing"
"rsc.io/qr"
)
const qrTestURI = "otpauth://totp/Privatae%20NAC%3AWORKSTATION-01%20for%20member" +
"?secret=JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP&issuer=Privatae+NAC&digits=6&period=30"
// parseRendered turns qrTerminal's output back into a module grid, so the test
// asserts against what a CAMERA would see rather than against the library call.
// The packing and the quiet zone are this repo's code; a bug there produces a
// block that looks perfectly fine on screen and never scans, which is exactly
// the failure this feature exists to remove.
func parseRendered(t *testing.T, s string) [][]bool {
t.Helper()
var rows [][]bool
for _, line := range strings.Split(s, "\n") {
// strip the SGR sequences
for {
i := strings.Index(line, "\x1b[")
if i < 0 {
break
}
j := strings.Index(line[i:], "m")
if j < 0 {
break
}
line = line[:i] + line[i+j+1:]
}
if line == "" {
continue
}
var top, bot []bool
for _, ch := range line {
switch ch {
case '█':
top, bot = append(top, true), append(bot, true)
case '▀':
top, bot = append(top, true), append(bot, false)
case '▄':
top, bot = append(top, false), append(bot, true)
default:
top, bot = append(top, false), append(bot, false)
}
}
rows = append(rows, top, bot)
}
return rows
}
func TestQRTerminalRoundTrip(t *testing.T) {
out, err := qrTerminal(qrTestURI)
if err != nil {
t.Fatalf("qrTerminal: %v", err)
}
grid := parseRendered(t, out)
c, err := qr.Encode(qrTestURI, qr.M)
if err != nil {
t.Fatal(err)
}
const quiet = 4
want := c.Size + 2*quiet
if len(grid) < want {
t.Fatalf("rendered %d module rows, need at least %d (symbol %d + 2x%d quiet)",
len(grid), want, c.Size, quiet)
}
if got := len(grid[0]); got != want {
t.Fatalf("rendered %d module columns, want %d — quiet zone wrong, scanners will not find the symbol", got, want)
}
// 1. Every module must survive the half-block packing unchanged. A single
// inverted or shifted module is an unscannable code.
for y := 0; y < c.Size; y++ {
for x := 0; x < c.Size; x++ {
if grid[y+quiet][x+quiet] != c.Black(x, y) {
t.Fatalf("module (%d,%d) rendered %v, want %v — packing corrupts the symbol",
x, y, grid[y+quiet][x+quiet], c.Black(x, y))
}
}
}
// 2. The quiet zone must be genuinely clear on all four sides. This is the
// single most common reason a hand-rolled terminal QR does not scan.
for y := 0; y < want; y++ {
for x := 0; x < want; x++ {
inSymbol := x >= quiet && x < quiet+c.Size && y >= quiet && y < quiet+c.Size
if !inSymbol && grid[y][x] {
t.Fatalf("dark module at (%d,%d) inside the quiet zone", x, y)
}
}
}
// 3. Orientation: the three finder patterns must be dark at their centres.
// If this passes while the visual is inverted, the ANSI colours are wrong
// — hence the explicit colour assertion below rather than only this.
for _, p := range [][2]int{{3, 3}, {c.Size - 4, 3}, {3, c.Size - 4}} {
if !c.Black(p[0], p[1]) {
t.Fatalf("finder centre (%d,%d) is not dark — wrong orientation", p[0], p[1])
}
}
// 4. Colours are set explicitly, never inherited. A dark-themed terminal
// would otherwise render an inverted code that most apps refuse.
if !strings.Contains(out, "\x1b[47;30m") {
t.Fatal("renderer must force white-bg/black-fg — an inherited dark theme inverts the code")
}
if !strings.HasSuffix(strings.TrimRight(out, "\n"), "\x1b[0m") {
t.Fatal("every row must reset SGR, or colour bleeds into the rest of the terminal")
}
}
// Writes the real rendered output for verification with an EXTERNAL decoder:
//
// QR_DUMP_URI='otpauth://…' go test -run TestQRDumpForExternalDecode
// # then reconstruct the grid and: zbarimg --raw /tmp/qr.pbm
//
// Confirmed: zbarimg read back the exact URI, byte for byte.
func TestQRDumpForExternalDecode(t *testing.T) {
uri := os.Getenv("QR_DUMP_URI")
if uri == "" {
t.Skip("QR_DUMP_URI unset")
}
out, err := qrTerminal(uri)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile("/tmp/qr.ansi", []byte(out), 0o644); err != nil {
t.Fatal(err)
}
}