1package typesafe23import (4 jsonv2 "encoding/json/v2"5 "fmt"6 "reflect"7 "strings"8 "testing"9)1011func TestQuestionMarshal(t *testing.T) {12 tests := []struct {13 name string14 q Question15 want string16 }{17 {"noul", NoulQuestion{Instructions: "Is it urgent?", Criteria: &NoulCriteria{True: "urgent", False: nil}}, `{"type":"noul","instructions":"Is it urgent?","criteria":{"true":"urgent","false":null}}`},18 {"choice", ChoiceQuestion{Instructions: "Pick one", Criteria: map[string]any{"a": nil, "b": "B"}}, `{"type":"choice","instructions":"Pick one","criteria":{"a":null,"b":"B"}}`},19 {"score", ScoreQuestion{Instructions: "Rate it", Criteria: []any{"low", "high"}}, `{"type":"score","instructions":"Rate it","criteria":["low","high"]}`},20 }21 for _, tt := range tests {22 t.Run(tt.name, func(t *testing.T) {23 got, err := jsonv2.Marshal(tt.q)24 if err != nil {25 t.Fatal(err)26 }27 var gotValue, wantValue any28 if err := jsonv2.Unmarshal(got, &gotValue); err != nil {29 t.Fatal(err)30 }31 if err := jsonv2.Unmarshal([]byte(tt.want), &wantValue); err != nil {32 t.Fatal(err)33 }34 if !reflect.DeepEqual(gotValue, wantValue) {35 t.Fatalf("got %s, want %s", got, tt.want)36 }37 })38 }39}4041func TestQuestionUnmarshal(t *testing.T) {42 q, err := UnmarshalQuestion([]byte(`{"type":"choice","instructions":"Pick","criteria":{"yes":null,"no":null}}`))43 if err != nil {44 t.Fatal(err)45 }46 if _, ok := q.(ChoiceQuestion); !ok {47 t.Fatalf("got %T", q)48 }49 if _, err := UnmarshalQuestion([]byte(`{"type":"future","instructions":"x"}`)); err == nil || !strings.Contains(err.Error(), "unknown question type") {50 t.Fatalf("got %v", err)51 }52}5354func TestRequestValidate(t *testing.T) {55 valid := SystemOneRequest{State: map[string]any{"x": "y"}, Questions: map[string]Question{"q": NoulQuestion{Instructions: "x"}}}56 if err := valid.Validate(); err != nil {57 t.Fatal(err)58 }59 cases := []SystemOneRequest{60 {State: 1, Questions: map[string]Question{"q": NoulQuestion{Instructions: "x"}}},61 {State: nil, Questions: map[string]Question{}},62 {State: nil, Questions: map[string]Question{"q": ChoiceQuestion{Criteria: map[string]any{"only": nil}}}},63 {State: nil, Questions: map[string]Question{"q": ScoreQuestion{Criteria: []any{"only"}}}},64 }65 for i, request := range cases {66 if err := request.Validate(); err == nil {67 t.Fatalf("case %d: expected error", i)68 }69 }70}7172func TestRequestValidateServiceLimits(t *testing.T) {73 labels := func(n int) map[string]any {74 criteria := make(map[string]any, n)75 for i := range n {76 criteria[fmt.Sprintf("o%d", i)] = nil77 }78 return criteria79 }80 levels := func(n int) []any {81 criteria := make([]any, n)82 for i := range criteria {83 criteria[i] = fmt.Sprintf("l%d", i)84 }85 return criteria86 }87 var nilState *requestState88 tests := []struct {89 name string90 state any91 q Question92 wantErr string93 }{94 {"max choice labels", "x", ChoiceQuestion{Instructions: "x", Criteria: labels(MaxChoiceOptions)}, ""},95 {"too many choice labels", "x", ChoiceQuestion{Instructions: "x", Criteria: labels(MaxChoiceOptions + 1)}, "at most 255 labels"},96 {"max score levels", "x", ScoreQuestion{Instructions: "x", Criteria: levels(MaxScoreLevels)}, ""},97 {"too many score levels", "x", ScoreQuestion{Instructions: "x", Criteria: levels(MaxScoreLevels + 1)}, "at most 10 levels"},98 {"null score level", "x", ScoreQuestion{Instructions: "x", Criteria: []any{"low", nil}}, "must not be null"},99 {"null choice instructions", "x", ChoiceQuestion{Criteria: labels(2)}, ""},100 {"noul criteria only", "x", NoulQuestion{Criteria: &NoulCriteria{True: "yes"}}, ""},101 {"noul without description", "x", NoulQuestion{Criteria: &NoulCriteria{}}, "requires instructions or criteria"},102 {"noul empty", "x", NoulQuestion{}, "requires instructions or criteria"},103 {"nil state", nil, NoulQuestion{Instructions: "x"}, "state is required"},104 {"nil pointer state", nilState, NoulQuestion{Instructions: "x"}, "state is required"},105 {"empty string state", "", NoulQuestion{Instructions: "x"}, ""},106 }107 for _, tt := range tests {108 t.Run(tt.name, func(t *testing.T) {109 err := SystemOneRequest{State: tt.state, Questions: map[string]Question{"q": tt.q}}.Validate()110 if tt.wantErr == "" {111 if err != nil {112 t.Fatalf("unexpected error: %v", err)113 }114 return115 }116 if err == nil || !strings.Contains(err.Error(), tt.wantErr) {117 t.Fatalf("got %v, want error containing %q", err, tt.wantErr)118 }119 })120 }121}122123type requestState struct {124 Name string `json:"name"`125}126127type scalarState struct{}128129func (scalarState) MarshalJSON() ([]byte, error) { return []byte("true"), nil }130131func TestRequestValidateStructuredState(t *testing.T) {132 for _, state := range []any{requestState{Name: "ok"}, &requestState{Name: "ok"}, []string{"ok"}} {133 request := SystemOneRequest{State: state, Questions: map[string]Question{"q": NoulQuestion{Instructions: "x"}}}134 if err := request.Validate(); err != nil {135 t.Fatalf("state %T: %v", state, err)136 }137 }138 request := SystemOneRequest{State: scalarState{}, Questions: map[string]Question{"q": NoulQuestion{Instructions: "x"}}}139 if err := request.Validate(); err == nil {140 t.Fatal("custom scalar state should be rejected")141 }142}143144func TestQuestionRejectsDuplicateAndInvalidUTF8(t *testing.T) {145 for _, data := range [][]byte{146 []byte(`{"type":"noul","type":"choice"}`),147 []byte{'{', '"', 't', 'y', 'p', 'e', '"', ':', '"', 0xff, '"', '}'},148 } {149 if _, err := UnmarshalQuestion(data); err == nil {150 t.Fatalf("expected malformed JSON error for %q", data)151 }152 }153}154155func TestRequestValidateQuestionPointers(t *testing.T) {156 noul := &NoulQuestion{Instructions: "x"}157 choice := &ChoiceQuestion{Instructions: "x", Criteria: map[string]any{"a": nil, "b": nil}}158 score := &ScoreQuestion{Instructions: "x", Criteria: []any{"a", "b"}}159 for name, question := range map[string]Question{"noul": noul, "choice": choice, "score": score} {160 request := SystemOneRequest{State: "x", Questions: map[string]Question{name: question}}161 if err := request.Validate(); err != nil {162 t.Fatalf("%s: %v", name, err)163 }164 }165 var nilNoul *NoulQuestion166 if err := (SystemOneRequest{State: "x", Questions: map[string]Question{"q": nilNoul}}).Validate(); err == nil {167 t.Fatal("typed nil question should be rejected")168 }169}