mirror of https://github.com/vernonkeenan/lib
653 lines
26 KiB
Go
653 lines
26 KiB
Go
package members_client_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"code.tnxs.net/vernonkeenan/lib/api/members/members_client/courses"
|
|
"code.tnxs.net/vernonkeenan/lib/api/members/members_client/enrollments"
|
|
"code.tnxs.net/vernonkeenan/lib/api/members/members_client/issued_certificates"
|
|
"code.tnxs.net/vernonkeenan/lib/api/members/members_client/lesson_progress"
|
|
"code.tnxs.net/vernonkeenan/lib/api/members/members_models"
|
|
openapiruntime "github.com/go-openapi/runtime"
|
|
"github.com/go-openapi/strfmt"
|
|
)
|
|
|
|
const membersLearningSpecSHA256 = "546c86af359490c898a2ab4fad2e0e96e9051fd854a5fcd07a56093a4121a891"
|
|
|
|
var learningTestAuth = openapiruntime.ClientAuthInfoWriterFunc(func(openapiruntime.ClientRequest, strfmt.Registry) error {
|
|
return nil
|
|
})
|
|
|
|
type learningCaptureTransport struct {
|
|
operation *openapiruntime.ClientOperation
|
|
}
|
|
|
|
func (transport *learningCaptureTransport) Submit(operation *openapiruntime.ClientOperation) (any, error) {
|
|
return transport.SubmitContext(context.Background(), operation)
|
|
}
|
|
|
|
func (transport *learningCaptureTransport) SubmitContext(_ context.Context, operation *openapiruntime.ClientOperation) (any, error) {
|
|
transport.operation = operation
|
|
|
|
switch operation.ID {
|
|
case "getCourses":
|
|
return courses.NewGetCoursesOK(), nil
|
|
case "postCourses":
|
|
return courses.NewPostCoursesOK(), nil
|
|
case "updateCourses":
|
|
return courses.NewUpdateCoursesOK(), nil
|
|
case "getCourseSections":
|
|
return courses.NewGetCourseSectionsOK(), nil
|
|
case "postCourseSections":
|
|
return courses.NewPostCourseSectionsOK(), nil
|
|
case "updateCourseSections":
|
|
return courses.NewUpdateCourseSectionsOK(), nil
|
|
case "getCourseLessons":
|
|
return courses.NewGetCourseLessonsOK(), nil
|
|
case "postCourseLessons":
|
|
return courses.NewPostCourseLessonsOK(), nil
|
|
case "updateCourseLessons":
|
|
return courses.NewUpdateCourseLessonsOK(), nil
|
|
case "getEnrollments":
|
|
return enrollments.NewGetEnrollmentsOK(), nil
|
|
case "postEnrollments":
|
|
return enrollments.NewPostEnrollmentsOK(), nil
|
|
case "putEnrollments":
|
|
return enrollments.NewPutEnrollmentsOK(), nil
|
|
case "getLessonProgresses":
|
|
return lesson_progress.NewGetLessonProgressesOK(), nil
|
|
case "postLessonProgresses":
|
|
return lesson_progress.NewPostLessonProgressesOK(), nil
|
|
case "putLessonProgresses":
|
|
return lesson_progress.NewPutLessonProgressesOK(), nil
|
|
case "getIssuedCertificates":
|
|
return issued_certificates.NewGetIssuedCertificatesOK(), nil
|
|
default:
|
|
panic("unexpected generated Learning operation: " + operation.ID)
|
|
}
|
|
}
|
|
|
|
func TestGeneratedLearningOperationContract(t *testing.T) {
|
|
type operationCall func(openapiruntime.ContextualTransport) error
|
|
|
|
tests := []struct {
|
|
name string
|
|
wantID string
|
|
wantMethod string
|
|
wantPath string
|
|
call operationCall
|
|
}{
|
|
{
|
|
name: "list courses", wantID: "getCourses", wantMethod: "GET", wantPath: "/courses",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).GetCourses(courses.NewGetCoursesParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "create course", wantID: "postCourses", wantMethod: "POST", wantPath: "/courses",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).PostCourses(courses.NewPostCoursesParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "update course", wantID: "updateCourses", wantMethod: "PUT", wantPath: "/courses",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).UpdateCourses(courses.NewUpdateCoursesParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "list sections", wantID: "getCourseSections", wantMethod: "GET", wantPath: "/coursesections",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).GetCourseSections(courses.NewGetCourseSectionsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "create section", wantID: "postCourseSections", wantMethod: "POST", wantPath: "/coursesections",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).PostCourseSections(courses.NewPostCourseSectionsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "update section", wantID: "updateCourseSections", wantMethod: "PUT", wantPath: "/coursesections",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).UpdateCourseSections(courses.NewUpdateCourseSectionsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "list lessons", wantID: "getCourseLessons", wantMethod: "GET", wantPath: "/courselessons",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).GetCourseLessons(courses.NewGetCourseLessonsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "create lesson", wantID: "postCourseLessons", wantMethod: "POST", wantPath: "/courselessons",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).PostCourseLessons(courses.NewPostCourseLessonsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "update lesson", wantID: "updateCourseLessons", wantMethod: "PUT", wantPath: "/courselessons",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := courses.New(transport, strfmt.Default).UpdateCourseLessons(courses.NewUpdateCourseLessonsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "list enrollments", wantID: "getEnrollments", wantMethod: "GET", wantPath: "/enrollments",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := enrollments.New(transport, strfmt.Default).GetEnrollments(enrollments.NewGetEnrollmentsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "create enrollment", wantID: "postEnrollments", wantMethod: "POST", wantPath: "/enrollments",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := enrollments.New(transport, strfmt.Default).PostEnrollments(enrollments.NewPostEnrollmentsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "update enrollment", wantID: "putEnrollments", wantMethod: "PUT", wantPath: "/enrollments",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := enrollments.New(transport, strfmt.Default).PutEnrollments(enrollments.NewPutEnrollmentsParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "list lesson progress", wantID: "getLessonProgresses", wantMethod: "GET", wantPath: "/lessonprogress",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := lesson_progress.New(transport, strfmt.Default).GetLessonProgresses(lesson_progress.NewGetLessonProgressesParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "create lesson progress", wantID: "postLessonProgresses", wantMethod: "POST", wantPath: "/lessonprogress",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := lesson_progress.New(transport, strfmt.Default).PostLessonProgresses(lesson_progress.NewPostLessonProgressesParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "update lesson progress", wantID: "putLessonProgresses", wantMethod: "PUT", wantPath: "/lessonprogress",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := lesson_progress.New(transport, strfmt.Default).PutLessonProgresses(lesson_progress.NewPutLessonProgressesParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
{
|
|
name: "list issued certificates", wantID: "getIssuedCertificates", wantMethod: "GET", wantPath: "/issuedcertificates",
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
|
_, err := issued_certificates.New(transport, strfmt.Default).GetIssuedCertificates(issued_certificates.NewGetIssuedCertificatesParams(), learningTestAuth)
|
|
return err
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
transport := &learningCaptureTransport{}
|
|
if err := test.call(transport); err != nil {
|
|
t.Fatalf("generated client operation failed: %v", err)
|
|
}
|
|
if transport.operation == nil {
|
|
t.Fatal("generated client did not submit an operation")
|
|
}
|
|
if transport.operation.ID != test.wantID {
|
|
t.Errorf("operation ID = %q, want %q", transport.operation.ID, test.wantID)
|
|
}
|
|
if transport.operation.Method != test.wantMethod {
|
|
t.Errorf("method = %q, want %q", transport.operation.Method, test.wantMethod)
|
|
}
|
|
if transport.operation.PathPattern != test.wantPath {
|
|
t.Errorf("path = %q, want %q", transport.operation.PathPattern, test.wantPath)
|
|
}
|
|
if transport.operation.AuthInfo == nil {
|
|
t.Error("generated operation discarded its auth-info writer")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLearningSpecRequiresExactCompoundAuthentication(t *testing.T) {
|
|
specText := readMembersLearningSpec(t)
|
|
for _, definition := range []string{
|
|
" ApiKeyAuth:\n type: \"apiKey\"\n in: \"header\"\n name: \"X-API-Key\"",
|
|
" kvSessionCookie:\n name: Cookie\n in: header\n type: apiKey",
|
|
} {
|
|
if !strings.Contains(specText, definition) {
|
|
t.Fatalf("authoritative spec is missing exact authentication definition:\n%s", definition)
|
|
}
|
|
}
|
|
|
|
operations := map[string]map[string]string{
|
|
"/courses": {
|
|
"get": "getCourses",
|
|
"post": "postCourses",
|
|
"put": "updateCourses",
|
|
},
|
|
"/coursesections": {
|
|
"get": "getCourseSections",
|
|
"post": "postCourseSections",
|
|
"put": "updateCourseSections",
|
|
},
|
|
"/courselessons": {
|
|
"get": "getCourseLessons",
|
|
"post": "postCourseLessons",
|
|
"put": "updateCourseLessons",
|
|
},
|
|
"/enrollments": {
|
|
"get": "getEnrollments",
|
|
"post": "postEnrollments",
|
|
"put": "putEnrollments",
|
|
},
|
|
"/lessonprogress": {
|
|
"get": "getLessonProgresses",
|
|
"post": "postLessonProgresses",
|
|
"put": "putLessonProgresses",
|
|
},
|
|
"/issuedcertificates": {
|
|
"get": "getIssuedCertificates",
|
|
},
|
|
}
|
|
|
|
for path, methods := range operations {
|
|
for method, operationID := range methods {
|
|
operation := learningSpecOperationBlock(t, specText, path, method)
|
|
if !strings.Contains(operation, " operationId: "+operationID+"\n") {
|
|
t.Errorf("%s %s does not preserve operationId %q", strings.ToUpper(method), path, operationID)
|
|
}
|
|
const compoundSecurity = " security:\n - ApiKeyAuth: []\n kvSessionCookie: []"
|
|
if strings.Count(operation, compoundSecurity) != 1 {
|
|
t.Errorf("%s %s does not require the exact API-key + human-session compound security", strings.ToUpper(method), path)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLearningUpdateClientsPreserveConflictResponses(t *testing.T) {
|
|
conflicts := []interface{ IsCode(int) bool }{
|
|
courses.NewUpdateCoursesConflict(),
|
|
courses.NewUpdateCourseSectionsConflict(),
|
|
courses.NewUpdateCourseLessonsConflict(),
|
|
enrollments.NewPutEnrollmentsConflict(),
|
|
lesson_progress.NewPutLessonProgressesConflict(),
|
|
}
|
|
for _, conflict := range conflicts {
|
|
if !conflict.IsCode(409) {
|
|
t.Errorf("%T does not preserve the optimistic-conflict response", conflict)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLearningGetOperationsPreserveIDFilters(t *testing.T) {
|
|
recordID := "record-id"
|
|
filters := []struct {
|
|
name string
|
|
got *string
|
|
}{
|
|
{name: "Course", got: courses.NewGetCoursesParams().WithID(&recordID).ID},
|
|
{name: "CourseSection", got: courses.NewGetCourseSectionsParams().WithID(&recordID).ID},
|
|
{name: "CourseLesson", got: courses.NewGetCourseLessonsParams().WithID(&recordID).ID},
|
|
{name: "Enrollment", got: enrollments.NewGetEnrollmentsParams().WithID(&recordID).ID},
|
|
{name: "LessonProgress", got: lesson_progress.NewGetLessonProgressesParams().WithID(&recordID).ID},
|
|
{name: "IssuedCertificate", got: issued_certificates.NewGetIssuedCertificatesParams().WithID(&recordID).ID},
|
|
}
|
|
for _, filter := range filters {
|
|
if filter.got == nil || *filter.got != recordID {
|
|
t.Errorf("%s get-by-ID filter is absent", filter.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLearningModelsMatchSanitizedProviderContract(t *testing.T) {
|
|
assertExactModelFields(t, members_models.Course{}, map[string]string{
|
|
"CreatedByID": "*string", "CreatedDate": "*string", "Description": "*string",
|
|
"Fulldescription": "*string", "ID": "string", "ImageAltText": "*string",
|
|
"ImageURL": "*string", "InstructorID": "*string", "LastModifiedByID": "*string",
|
|
"LastModifiedDate": "*string", "Logo": "*string", "Price": "*string",
|
|
"Slug": "*string", "TemplateID": "*string", "Title": "*string",
|
|
})
|
|
assertExactModelFields(t, members_models.CourseSection{}, map[string]string{
|
|
"Content": "*string", "CourseID": "*string", "CreatedByID": "*string",
|
|
"CreatedDate": "*string", "ID": "string", "ImageAltText": "*string",
|
|
"ImageURL": "*string", "LastModifiedByID": "*string", "LastModifiedDate": "*string",
|
|
"Logo": "*string", "Order": "*int64", "Slug": "*string", "Title": "*string",
|
|
})
|
|
assertExactModelFields(t, members_models.CourseLesson{}, map[string]string{
|
|
"Content": "*string", "CreatedByID": "*string", "CreatedDate": "*string",
|
|
"ID": "string", "ImageAltText": "*string", "ImageURL": "*string",
|
|
"LastModifiedByID": "*string", "LastModifiedDate": "*string", "Logo": "*string",
|
|
"Order": "*int64", "SectionID": "*string", "Slug": "*string",
|
|
"Title": "*string", "VideoURL": "*string",
|
|
})
|
|
assertExactModelFields(t, members_models.Enrollment{}, map[string]string{
|
|
"Completed": "*bool", "CourseID": "*string", "CreatedByID": "*string",
|
|
"CreatedDate": "*string", "EnrollmentDate": "*strfmt.Date", "ID": "string",
|
|
"LastModifiedByID": "*string", "LastModifiedDate": "*string", "UserID": "*string",
|
|
})
|
|
assertExactModelFields(t, members_models.LessonProgress{}, map[string]string{
|
|
"Completed": "bool", "CompletedAt": "*string", "CreatedByID": "*string",
|
|
"CreatedDate": "*string", "EnrollmentID": "*string", "ID": "string",
|
|
"LastModifiedByID": "*string", "LastModifiedDate": "*string", "LessonID": "*string",
|
|
"TenantID": "*string", "UserID": "*string",
|
|
})
|
|
assertExactModelFields(t, members_models.IssuedCertificate{}, map[string]string{
|
|
"AccountID": "*string", "CreatedByID": "*string", "CreatedDate": "*string",
|
|
"EnrollmentID": "*string", "ExpirationDate": "*string", "ID": "string",
|
|
"IssueDate": "*string", "LastModifiedByID": "*string", "LastModifiedDate": "*string",
|
|
"TemplateID": "*string", "UserID": "*string",
|
|
})
|
|
|
|
price := "125.50"
|
|
if err := (&members_models.Course{Price: &price}).Validate(strfmt.Default); err != nil {
|
|
t.Fatalf("valid exact-decimal Course.Price rejected: %v", err)
|
|
}
|
|
unsafePrice := "125.505"
|
|
if err := (&members_models.Course{Price: &unsafePrice}).Validate(strfmt.Default); err == nil {
|
|
t.Fatal("Course.Price accepted a value outside DECIMAL(10,2)")
|
|
}
|
|
|
|
body, err := json.Marshal(members_models.IssuedCertificate{ID: "issued-certificate-id"})
|
|
if err != nil {
|
|
t.Fatalf("marshal IssuedCertificate: %v", err)
|
|
}
|
|
for _, forbidden := range []string{"VerificationCode", "CertificateID"} {
|
|
if bytes.Contains(body, []byte(forbidden)) {
|
|
t.Errorf("sanitized IssuedCertificate JSON exposes %s: %s", forbidden, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLearningSingleRecordRequestsRejectBulkPayloads(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
validate func() error
|
|
}{
|
|
{
|
|
name: "course",
|
|
validate: func() error {
|
|
return (&members_models.CourseRequest{Data: []*members_models.Course{{}, {}}}).Validate(strfmt.Default)
|
|
},
|
|
},
|
|
{
|
|
name: "course section",
|
|
validate: func() error {
|
|
return (&members_models.CourseSectionRequest{Data: []*members_models.CourseSection{{}, {}}}).Validate(strfmt.Default)
|
|
},
|
|
},
|
|
{
|
|
name: "course lesson",
|
|
validate: func() error {
|
|
return (&members_models.CourseLessonRequest{Data: []*members_models.CourseLesson{{}, {}}}).Validate(strfmt.Default)
|
|
},
|
|
},
|
|
{
|
|
name: "enrollment",
|
|
validate: func() error {
|
|
return (&members_models.EnrollmentRequest{Data: []*members_models.Enrollment{{}, {}}}).Validate(strfmt.Default)
|
|
},
|
|
},
|
|
{
|
|
name: "lesson progress",
|
|
validate: func() error {
|
|
return (&members_models.LessonProgressRequest{Data: []*members_models.LessonProgress{{}, {}}}).Validate(strfmt.Default)
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if err := test.validate(); err == nil {
|
|
t.Fatal("generated single-record request accepted a bulk payload")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLearningLifecycleBoundariesRemainVisible(t *testing.T) {
|
|
specText := readMembersLearningSpec(t)
|
|
expectations := []struct {
|
|
path string
|
|
method string
|
|
terms []string
|
|
}{
|
|
{path: "/coursesections", method: "put", terms: []string{"CourseID is immutable"}},
|
|
{path: "/courselessons", method: "put", terms: []string{"SectionID is immutable"}},
|
|
{path: "/enrollments", method: "put", terms: []string{"UserID and CourseID are immutable", "completion is monotonic"}},
|
|
{path: "/lessonprogress", method: "put", terms: []string{"TenantID, UserID, EnrollmentID, and LessonID are immutable", "completion is monotonic", "CompletedAt is server-owned"}},
|
|
{path: "/issuedcertificates", method: "get", terms: []string{"verification material is never returned", "Issuance, mutation, revocation, and deletion", "intentionally unavailable"}},
|
|
}
|
|
for _, expectation := range expectations {
|
|
block := learningSpecOperationBlock(t, specText, expectation.path, expectation.method)
|
|
for _, term := range expectation.terms {
|
|
if !strings.Contains(block, term) {
|
|
t.Errorf("%s %s lost lifecycle boundary %q", strings.ToUpper(expectation.method), expectation.path, term)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLearningSurfaceOmitsUnsafeCertificateAndDeleteOperations(t *testing.T) {
|
|
contracts := []struct {
|
|
name string
|
|
typ reflect.Type
|
|
}{
|
|
{name: "Courses", typ: reflect.TypeOf((*courses.ClientService)(nil)).Elem()},
|
|
{name: "Enrollments", typ: reflect.TypeOf((*enrollments.ClientService)(nil)).Elem()},
|
|
{name: "LessonProgress", typ: reflect.TypeOf((*lesson_progress.ClientService)(nil)).Elem()},
|
|
{name: "IssuedCertificates", typ: reflect.TypeOf((*issued_certificates.ClientService)(nil)).Elem()},
|
|
}
|
|
for _, contract := range contracts {
|
|
for index := 0; index < contract.typ.NumMethod(); index++ {
|
|
method := contract.typ.Method(index).Name
|
|
if strings.HasPrefix(method, "Delete") {
|
|
t.Errorf("%s client exposes unsupported delete operation %s", contract.name, method)
|
|
}
|
|
}
|
|
}
|
|
for _, method := range []string{
|
|
"PostIssuedCertificate", "PostIssuedCertificateContext",
|
|
"PostIssuedCertificates", "PostIssuedCertificatesContext",
|
|
"PutIssuedCertificate", "PutIssuedCertificateContext",
|
|
"PutIssuedCertificates", "PutIssuedCertificatesContext",
|
|
"DeleteIssuedCertificate", "DeleteIssuedCertificateContext",
|
|
} {
|
|
if _, exists := contracts[3].typ.MethodByName(method); exists {
|
|
t.Errorf("read-only IssuedCertificate client exposes forbidden operation %s", method)
|
|
}
|
|
}
|
|
|
|
repoRoot := learningRepoRoot(t)
|
|
certificateClientDir := filepath.Join(repoRoot, "api", "members", "members_client", "certificates")
|
|
if entries, err := os.ReadDir(certificateClientDir); err == nil {
|
|
for _, entry := range entries {
|
|
if strings.HasSuffix(entry.Name(), ".go") {
|
|
t.Errorf("removed /certificates generated client still contains %s", entry.Name())
|
|
}
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
t.Fatalf("inspect removed certificate client directory: %v", err)
|
|
}
|
|
for _, removed := range []string{
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "certificate.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "certificate_request.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "certificate_response.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "issued_certificate_request.go"),
|
|
} {
|
|
if _, err := os.Stat(removed); !os.IsNotExist(err) {
|
|
t.Errorf("removed unsafe generated surface still exists: %s", removed)
|
|
}
|
|
}
|
|
|
|
specText := readMembersLearningSpec(t)
|
|
if strings.Contains(specText, "\n /certificates:") {
|
|
t.Fatal("authoritative spec still exposes removed /certificates path")
|
|
}
|
|
issuedPath := learningSpecPathBlock(t, specText, "/issuedcertificates")
|
|
for _, forbidden := range []string{" post:", " put:", " patch:", " delete:"} {
|
|
if strings.Contains(issuedPath, forbidden) {
|
|
t.Fatalf("authoritative IssuedCertificate contract exposes forbidden operation %q", strings.TrimSpace(forbidden))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMembersSpecIsPinnedToLearningProviderSource(t *testing.T) {
|
|
repoRoot := learningRepoRoot(t)
|
|
mainSpec, err := os.ReadFile(filepath.Join(repoRoot, "swagger", "members-vernonkeenan.yaml"))
|
|
if err != nil {
|
|
t.Fatalf("read Members spec: %v", err)
|
|
}
|
|
sum := sha256.Sum256(mainSpec)
|
|
if got := hex.EncodeToString(sum[:]); got != membersLearningSpecSHA256 {
|
|
t.Fatalf("Members spec drifted from Learning merge fb25d140: SHA-256 = %s, want %s", got, membersLearningSpecSHA256)
|
|
}
|
|
|
|
externalSpec, err := os.ReadFile(filepath.Join(repoRoot, "swagger", "external", "members-vernonkeenan.yaml"))
|
|
if err != nil {
|
|
t.Fatalf("read external Members spec: %v", err)
|
|
}
|
|
normalized := bytes.ReplaceAll(externalSpec, []byte(`"https"`), []byte(`"http"`))
|
|
normalized = bytes.ReplaceAll(normalized, []byte("gw.tnxs.net"), []byte("members.vernonkeenan.com:8080"))
|
|
normalized = bytes.ReplaceAll(normalized, []byte(`"/vk/members/v1"`), []byte(`"/v1"`))
|
|
if !bytes.Equal(normalized, mainSpec) {
|
|
t.Fatal("external Members spec differs from the conventional host/scheme/base-path rewrite")
|
|
}
|
|
}
|
|
|
|
func TestGeneratedLearningSurfaceHasNoSalesforceOrEmbeddedSecrets(t *testing.T) {
|
|
repoRoot := learningRepoRoot(t)
|
|
targets := []string{
|
|
filepath.Join(repoRoot, "api", "members", "members_client", "courses"),
|
|
filepath.Join(repoRoot, "api", "members", "members_client", "enrollments"),
|
|
filepath.Join(repoRoot, "api", "members", "members_client", "lesson_progress"),
|
|
filepath.Join(repoRoot, "api", "members", "members_client", "issued_certificates"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "course.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "course_request.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "course_section.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "course_section_request.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "course_lesson.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "course_lesson_request.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "enrollment.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "enrollment_request.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "lesson_progress.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "lesson_progress_request.go"),
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "issued_certificate.go"),
|
|
}
|
|
credentialLiteral := regexp.MustCompile(`(?i)(api[_-]?key|password|secret)\s*[:=]\s*["'][^"']+["']`)
|
|
|
|
for _, target := range targets {
|
|
err := filepath.WalkDir(target, func(path string, entry os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lower := strings.ToLower(string(content))
|
|
for _, forbidden := range []string{"salesforce", "sf-gate", "go-force", "private key-----"} {
|
|
if strings.Contains(lower, forbidden) {
|
|
t.Errorf("%s contains forbidden boundary %q", path, forbidden)
|
|
}
|
|
}
|
|
if credentialLiteral.Match(content) {
|
|
t.Errorf("%s contains a credential-shaped literal", path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("scan generated Learning target %s: %v", target, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertExactModelFields(t *testing.T, model any, want map[string]string) {
|
|
t.Helper()
|
|
modelType := reflect.TypeOf(model)
|
|
if modelType.NumField() != len(want) {
|
|
t.Errorf("%s has %d fields, want exact authoritative set of %d", modelType.Name(), modelType.NumField(), len(want))
|
|
}
|
|
for name, wantType := range want {
|
|
field, exists := modelType.FieldByName(name)
|
|
if !exists {
|
|
t.Errorf("%s is missing authoritative field %s", modelType.Name(), name)
|
|
continue
|
|
}
|
|
if field.Type.String() != wantType {
|
|
t.Errorf("%s.%s type = %s, want %s", modelType.Name(), name, field.Type, wantType)
|
|
}
|
|
if got := strings.Split(field.Tag.Get("json"), ",")[0]; got != name {
|
|
t.Errorf("%s.%s JSON name = %q, want %q", modelType.Name(), name, got, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func readMembersLearningSpec(t *testing.T) string {
|
|
t.Helper()
|
|
content, err := os.ReadFile(filepath.Join(learningRepoRoot(t), "swagger", "members-vernonkeenan.yaml"))
|
|
if err != nil {
|
|
t.Fatalf("read Members Swagger: %v", err)
|
|
}
|
|
return string(content)
|
|
}
|
|
|
|
func learningSpecPathBlock(t *testing.T, specText, path string) string {
|
|
t.Helper()
|
|
startMarker := " " + path + ":\n"
|
|
start := strings.Index(specText, startMarker)
|
|
if start < 0 {
|
|
t.Fatalf("authoritative spec is missing path %s", path)
|
|
}
|
|
block := specText[start+len(startMarker):]
|
|
if end := strings.Index(block, "\n /"); end >= 0 {
|
|
block = block[:end]
|
|
}
|
|
return block
|
|
}
|
|
|
|
func learningSpecOperationBlock(t *testing.T, specText, path, method string) string {
|
|
t.Helper()
|
|
pathBlock := learningSpecPathBlock(t, specText, path)
|
|
startMarker := " " + method + ":\n"
|
|
start := strings.Index(pathBlock, startMarker)
|
|
if start < 0 {
|
|
t.Fatalf("authoritative spec is missing %s %s", strings.ToUpper(method), path)
|
|
}
|
|
block := pathBlock[start+len(startMarker):]
|
|
methodBoundary := regexp.MustCompile(`(?m)^ [a-z]+:\n`)
|
|
if end := methodBoundary.FindStringIndex(block); end != nil {
|
|
block = block[:end[0]]
|
|
}
|
|
return block
|
|
}
|
|
|
|
func learningRepoRoot(t *testing.T) string {
|
|
t.Helper()
|
|
_, testFile, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("resolve test source path")
|
|
}
|
|
return filepath.Clean(filepath.Join(filepath.Dir(testFile), "..", "..", ".."))
|
|
}
|