Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions probeverdict/probeverdict.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Package probeverdict turns a geolocation probe submission into a verdict:
// verified, unverified, or suspect. It is pure decision logic with no I/O, so
// it is fully table-testable independent of how a submission arrived.
//
// Deliberately absent from Input: the mmdb-derived country for the same
// connection, and any RTT or coordinate fields. A probed country differing
// from what the free mmdb would have said is the entire point of this
// project and must never be treated as suspicious -- keeping that field off
// Input makes the omission structural. An RTT-distance corroboration was
// designed and dropped before implementation (see the spec's "The RTT floor
// was designed, then dropped"): it needs a fixed reference point that this
// system does not have a single answer for once more than one deployment
// instance exists, and a wrong reference point does not fail safe -- it can
// flag an honest provider as suspect. Do not reintroduce ObservedRTT or
// coordinate fields here without first resolving that reference-point
// problem in the spec.
package probeverdict

import "time"

type Input struct {
CountryConfident bool
CountryCode string

PreviousCountryCode string
PreviousObservedAt time.Time
Now time.Time
}

type Verdict struct {
State string
Reason string
}

// unstableWindow is how recently a prior, different country counts as a
// flip-flop rather than a legitimate correction.
const unstableWindow = 24 * time.Hour

func Evaluate(in Input) Verdict {
if !in.CountryConfident {
return Verdict{State: "unverified", Reason: "no_consensus"}
}

if in.PreviousCountryCode != "" && in.PreviousCountryCode != in.CountryCode {
now := in.Now
if now.IsZero() {
now = time.Now()
}
if now.Sub(in.PreviousObservedAt) < unstableWindow {
return Verdict{State: "suspect", Reason: "unstable"}
}
}

return Verdict{State: "verified"}
}
97 changes: 97 additions & 0 deletions probeverdict/probeverdict_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package probeverdict

import (
"reflect"
"testing"
"time"
)

func TestEvaluateNoConsensusIsUnverified(t *testing.T) {
v := Evaluate(Input{CountryConfident: false})
if v.State != "unverified" || v.Reason != "no_consensus" {
t.Errorf("got %+v, want unverified/no_consensus", v)
}
}

func TestEvaluateCountryFlipFlopIsSuspect(t *testing.T) {
now := time.Now()
v := Evaluate(Input{
CountryConfident: true,
CountryCode: "de",
PreviousCountryCode: "es",
PreviousObservedAt: now.Add(-2 * time.Hour),
Now: now,
})
if v.State != "suspect" || v.Reason != "unstable" {
t.Errorf("got %+v, want suspect/unstable", v)
}
}

func TestEvaluateCountryChangeOutsideWindowIsVerified(t *testing.T) {
// a country change is only "unstable" within the 24h window -- after it,
// a changed country is a legitimate correction, not a flip-flop
now := time.Now()
v := Evaluate(Input{
CountryConfident: true,
CountryCode: "de",
PreviousCountryCode: "es",
PreviousObservedAt: now.Add(-25 * time.Hour),
Now: now,
})
if v.State != "verified" {
t.Errorf("got %+v, want verified (change is outside the 24h window)", v)
}
}

func TestEvaluateMmdbDivergenceAloneIsNotSuspect(t *testing.T) {
// this test asserts the single most important safety property in the
// package: a country that differs from what mmdb would have said is NOT
// an input to Evaluate at all -- there is no field for it on Input, so a
// clean first-time probe always verifies regardless of what mmdb would
// have said about the same connection.
v := Evaluate(Input{
CountryConfident: true,
CountryCode: "es",
})
if v.State != "verified" {
t.Errorf("a clean probe with no prior history must verify regardless of what mmdb would have said, got %+v", v)
}
}

// TestInputHasNoMmdbRttOrCoordinateFields locks the two structural omissions
// this package depends on for correctness. Neither can be asserted
// behaviourally -- they are the absence of inputs, so the only way to test
// them is to pin the field set itself.
//
// 1. No mmdb-derived country. A probed country diverging from what the free
// mmdb would have said is the entire point of this project, and with no
// field for it a caller cannot get the rule wrong.
// 2. No RTT or coordinate fields. An RTT-distance floor was designed and
// deliberately dropped (see the package doc comment and the spec's "The
// RTT floor was designed, then dropped"): it needs a fixed reference
// point this system has no single answer for, and a wrong reference point
// does not fail safe -- it can flag an honest provider as suspect.
//
// If this test fails because a field was added, that is the point: resolve
// the reference-point problem in the spec first.
func TestInputHasNoMmdbRttOrCoordinateFields(t *testing.T) {
allowed := map[string]bool{
"CountryConfident": true,
"CountryCode": true,
"PreviousCountryCode": true,
"PreviousObservedAt": true,
"Now": true,
}

inputType := reflect.TypeOf(Input{})
for i := range inputType.NumField() {
name := inputType.Field(i).Name
if !allowed[name] {
t.Errorf("Input has an unexpected field %q: mmdb-country, RTT and coordinate "+
"fields are deliberately absent from Input; see the package doc comment", name)
}
}
if inputType.NumField() != len(allowed) {
t.Errorf("Input has %d fields, want exactly %d", inputType.NumField(), len(allowed))
}
}